Search code examples
phplaravel-5.6monolog

Trying to create custom log channel Laravel 5.6


Inside config/logging.php:

'channels' => [
        'stack' => [
            'driver' => 'stack',
            'channels' => ['single', 'mongo'],
        ],

        'mongo' => [
            'driver' => 'monolog',
            'handler' => \Monolog\Handler\MongoDBHandler::class,
            'handler_with' => [
                'mongo' => new MongoDB\Client(),
                'database' => 'logs',
                'collection' => 'test'
            ]
        ]
    ],

.env: LOG_CHANNEL=stack

I'm sure that MongoDB database logs exists, test collection too.

I'm trying to log some data being inside php artisan tinker:

Log::info('test');

I got exception

Expected $document to have type "array or object" but found "string"

Even I try php artisan config:cache I got this exception.

What can be wrong?


Update: 'mongo' => MongoDB\Client::class, causes the same error. By the way, there is no word in documentation about passing class either class instance inside handler_with array.

This config

'mongo' => [
            'driver' => 'monolog',
            'handler' => \Monolog\Handler\MongoDBHandler::class,
        ]

also returns error, but I expected InvalidArgumentException.

This one

'mongo' => [
    'driver' => 'monolog',
    'handler' => new \Monolog\Handler\MongoDBHandler(new MongoDB\Client(),'logs', 'prod'),
]

causes LogicException : Your configuration files are not serializable..


Solution

  • Sollution by mfn:
    add formatter to the channel array:

    'formatter' => \Monolog\Formatter\MongoDBFormatter::class,
    

    also create custom formatter class, because mongo formatter used in current Laravel version is out of date.

    <?php
    /*
     * This file is part of the Monolog package.
     *
     * (c) Jordi Boggiano <j.boggiano@seld.be>
     *
     * For the full copyright and license information, please view the LICENSE
     * file that was distributed with this source code.
     */
    namespace Monolog\Formatter;
    /**
     * Formats a record for use with the MongoDBHandler.
     *
     * @author Florian Plattner <me@florianplattner.de>
     */
    class MongoDBFormatter implements FormatterInterface
    {
        private $exceptionTraceAsString;
        private $maxNestingLevel;
        /**
         * @param int  $maxNestingLevel        0 means infinite nesting, the $record itself is level 1, $record['context'] is 2
         * @param bool $exceptionTraceAsString set to false to log exception traces as a sub documents instead of strings
         */
        public function __construct($maxNestingLevel = 3, $exceptionTraceAsString = true)
        {
            $this->maxNestingLevel = max($maxNestingLevel, 0);
            $this->exceptionTraceAsString = (bool) $exceptionTraceAsString;
        }
        /**
         * {@inheritDoc}
         */
        public function format(array $record)
        {
            return $this->formatArray($record);
        }
        /**
         * {@inheritDoc}
         */
        public function formatBatch(array $records)
        {
            foreach ($records as $key => $record) {
                $records[$key] = $this->format($record);
            }
            return $records;
        }
        protected function formatArray(array $record, $nestingLevel = 0)
        {
            if ($this->maxNestingLevel == 0 || $nestingLevel <= $this->maxNestingLevel) {
                foreach ($record as $name => $value) {
                    if ($value instanceof \DateTime) {
                        $record[$name] = $this->formatDate($value, $nestingLevel + 1);
                    } elseif ($value instanceof \Exception) {
                        $record[$name] = $this->formatException($value, $nestingLevel + 1);
                    } elseif (is_array($value)) {
                        $record[$name] = $this->formatArray($value, $nestingLevel + 1);
                    } elseif (is_object($value)) {
                        $record[$name] = $this->formatObject($value, $nestingLevel + 1);
                    }
                }
            } else {
                $record = '[...]';
            }
            return $record;
        }
        protected function formatObject($value, $nestingLevel)
        {
            $objectVars = get_object_vars($value);
            $objectVars['class'] = get_class($value);
            return $this->formatArray($objectVars, $nestingLevel);
        }
        protected function formatException(\Exception $exception, $nestingLevel)
        {
            $formattedException = array(
                'class' => get_class($exception),
                'message' => $exception->getMessage(),
                'code' => $exception->getCode(),
                'file' => $exception->getFile() . ':' . $exception->getLine(),
            );
            if ($this->exceptionTraceAsString === true) {
                $formattedException['trace'] = $exception->getTraceAsString();
            } else {
                $formattedException['trace'] = $exception->getTrace();
            }
            return $this->formatArray($formattedException, $nestingLevel);
        }
        protected function formatDate(\DateTime $value, $nestingLevel)
        {
            return new UTCDateTime($value->getTimestamp());
        }
    }
    

    UPDATE

    My own implementation so far:

    config/logging.php:

    'channels' => [
        'stack' => [
            'driver' => 'stack',
            'channels' => [... , 'mongo'],
        ],
    
        ...
    
        'mongo' => [
            'driver' => 'custom',
            'handler' => \Monolog\Handler\MongoDBHandler::class,
            'via' => \App\Logging\MongoLogger::class, //place MongoLogger where you want
            'name' => 'mongo',
            'host' => env('MONGODB_HOST'), //you can keep these right here in cofig, but I prefer .env because its scaleable
            'port' => env('MONGODB_PORT'),
            'database' => env('MONGODB_DATABASE'),
            'collection' => env('MONGODB_COLLECTION')
        ]
    ],
    

    MongoLogger class:

    <?php
    
    namespace App\Logging; //once again, palce this where you want
    
    use App\MongoDBFormatter;
    use MongoDB\Client;
    use Monolog\Handler\MongoDBHandler;
    use Monolog\Logger;
    
    class MongoLogger
    {
        public function __invoke(array $config)
        {
            $handler = new MongoDBHandler(
                new Client('mongodb://'.$config['host'].':'.$config['port']),
                $config['database'],
                $config['collection']
            );
    
            $handler->setFormatter(new MongoDBFormatter());
    
            return new Logger($config['name'], [$handler]);
        }
    }
    

    MongoDBFormatter class (modified a bit, original one with some deprecated classes, by the way):

    <?php
    /*
     * This file is part of the Monolog package.
     *
     * (c) Jordi Boggiano <j.boggiano@seld.be>
     *
     * For the full copyright and license information, please view the LICENSE
     * file that was distributed with this source code.
     */
    namespace App; //once again, palce this where you want
    
    use Monolog\Formatter\FormatterInterface;
    
    /**
     * Formats a record for use with the MongoDBHandler.
     *
     * @author Florian Plattner <me@florianplattner.de>
     */
    class MongoDBFormatter implements FormatterInterface
    {
        private $exceptionTraceAsString;
        private $maxNestingLevel;
        /**
         * @param int  $maxNestingLevel        0 means infinite nesting, the $record itself is level 1, $record['context'] is 2
         * @param bool $exceptionTraceAsString set to false to log exception traces as a sub documents instead of strings
         */
        public function __construct($maxNestingLevel = 3, $exceptionTraceAsString = true)
        {
            $this->maxNestingLevel = max($maxNestingLevel, 0);
            $this->exceptionTraceAsString = (bool) $exceptionTraceAsString;
        }
        /**
         * {@inheritDoc}
         */
        public function format(array $record)
        {
            return $this->formatArray($record);
        }
        /**
         * {@inheritDoc}
         */
        public function formatBatch(array $records)
        {
            foreach ($records as $key => $record) {
                $records[$key] = $this->format($record);
            }
            return $records;
        }
        protected function formatArray(array $record, $nestingLevel = 0)
        {
            if ($this->maxNestingLevel == 0 || $nestingLevel <= $this->maxNestingLevel) {
                foreach ($record as $name => $value) {
                    if ($value instanceof \DateTime) {
                        $record[$name] = $this->formatDate($value, $nestingLevel + 1);
                    } elseif ($value instanceof \Exception) {
                        $record[$name] = $this->formatException($value, $nestingLevel + 1);
                    } elseif (is_array($value)) {
                        $record[$name] = $this->formatArray($value, $nestingLevel + 1);
                    } elseif (is_object($value)) {
                        $record[$name] = $this->formatObject($value, $nestingLevel + 1);
                    }
                }
            } else {
                $record = '[...]';
            }
            return $record;
        }
        protected function formatObject($value, $nestingLevel)
        {
            $objectVars = get_object_vars($value);
            $objectVars['class'] = get_class($value);
            return $this->formatArray($objectVars, $nestingLevel);
        }
        protected function formatException(\Exception $exception, $nestingLevel)
        {
            $formattedException = array(
                'class' => get_class($exception),
                'message' => $exception->getMessage(),
                'code' => $exception->getCode(),
                'file' => $exception->getFile() . ':' . $exception->getLine(),
            );
            if ($this->exceptionTraceAsString === true) {
                $formattedException['trace'] = $exception->getTraceAsString();
            } else {
                $formattedException['trace'] = $exception->getTrace();
            }
            return $this->formatArray($formattedException, $nestingLevel);
        }
        protected function formatDate(\DateTime $value, $nestingLevel)
        {
            return $value;
        }
    }
    

    Answser source.