1<?php declare(strict_types=1);
2
3/*
4 * This file is part of the Monolog package.
5 *
6 * (c) Jordi Boggiano <j.boggiano@seld.be>
7 *
8 * For the full copyright and license information, please view the LICENSE
9 * file that was distributed with this source code.
10 */
11
12namespace Monolog\Formatter;
13
14use Monolog\Logger;
15
16/**
17 * Serializes a log message according to Wildfire's header requirements
18 *
19 * @author Eric Clemmons (@ericclemmons) <eric@uxdriven.com>
20 * @author Christophe Coevoet <stof@notk.org>
21 * @author Kirill chEbba Chebunin <iam@chebba.org>
22 *
23 * @phpstan-import-type Level from \Monolog\Logger
24 */
25class WildfireFormatter extends NormalizerFormatter
26{
27    /**
28     * Translates Monolog log levels to Wildfire levels.
29     *
30     * @var array<Level, string>
31     */
32    private $logLevels = [
33        Logger::DEBUG     => 'LOG',
34        Logger::INFO      => 'INFO',
35        Logger::NOTICE    => 'INFO',
36        Logger::WARNING   => 'WARN',
37        Logger::ERROR     => 'ERROR',
38        Logger::CRITICAL  => 'ERROR',
39        Logger::ALERT     => 'ERROR',
40        Logger::EMERGENCY => 'ERROR',
41    ];
42
43    /**
44     * @param string|null $dateFormat The format of the timestamp: one supported by DateTime::format
45     */
46    public function __construct(?string $dateFormat = null)
47    {
48        parent::__construct($dateFormat);
49
50        // http headers do not like non-ISO-8559-1 characters
51        $this->removeJsonEncodeOption(JSON_UNESCAPED_UNICODE);
52    }
53
54    /**
55     * {@inheritDoc}
56     *
57     * @return string
58     */
59    public function format(array $record): string
60    {
61        // Retrieve the line and file if set and remove them from the formatted extra
62        $file = $line = '';
63        if (isset($record['extra']['file'])) {
64            $file = $record['extra']['file'];
65            unset($record['extra']['file']);
66        }
67        if (isset($record['extra']['line'])) {
68            $line = $record['extra']['line'];
69            unset($record['extra']['line']);
70        }
71
72        /** @var mixed[] $record */
73        $record = $this->normalize($record);
74        $message = ['message' => $record['message']];
75        $handleError = false;
76        if ($record['context']) {
77            $message['context'] = $record['context'];
78            $handleError = true;
79        }
80        if ($record['extra']) {
81            $message['extra'] = $record['extra'];
82            $handleError = true;
83        }
84        if (count($message) === 1) {
85            $message = reset($message);
86        }
87
88        if (isset($record['context']['table'])) {
89            $type  = 'TABLE';
90            $label = $record['channel'] .': '. $record['message'];
91            $message = $record['context']['table'];
92        } else {
93            $type  = $this->logLevels[$record['level']];
94            $label = $record['channel'];
95        }
96
97        // Create JSON object describing the appearance of the message in the console
98        $json = $this->toJson([
99            [
100                'Type'  => $type,
101                'File'  => $file,
102                'Line'  => $line,
103                'Label' => $label,
104            ],
105            $message,
106        ], $handleError);
107
108        // The message itself is a serialization of the above JSON object + it's length
109        return sprintf(
110            '%d|%s|',
111            strlen($json),
112            $json
113        );
114    }
115
116    /**
117     * {@inheritDoc}
118     *
119     * @phpstan-return never
120     */
121    public function formatBatch(array $records)
122    {
123        throw new \BadMethodCallException('Batch formatting does not make sense for the WildfireFormatter');
124    }
125
126    /**
127     * {@inheritDoc}
128     *
129     * @return null|scalar|array<array|scalar|null>|object
130     */
131    protected function normalize($data, int $depth = 0)
132    {
133        if (is_object($data) && !$data instanceof \DateTimeInterface) {
134            return $data;
135        }
136
137        return parent::normalize($data, $depth);
138    }
139}
140