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 Throwable;
15
16/**
17 * Encodes whatever record data is passed to it as json
18 *
19 * This can be useful to log to databases or remote APIs
20 *
21 * @author Jordi Boggiano <j.boggiano@seld.be>
22 *
23 * @phpstan-import-type Record from \Monolog\Logger
24 */
25class JsonFormatter extends NormalizerFormatter
26{
27    public const BATCH_MODE_JSON = 1;
28    public const BATCH_MODE_NEWLINES = 2;
29
30    /** @var self::BATCH_MODE_* */
31    protected $batchMode;
32    /** @var bool */
33    protected $appendNewline;
34    /** @var bool */
35    protected $ignoreEmptyContextAndExtra;
36    /** @var bool */
37    protected $includeStacktraces = false;
38
39    /**
40     * @param self::BATCH_MODE_* $batchMode
41     */
42    public function __construct(int $batchMode = self::BATCH_MODE_JSON, bool $appendNewline = true, bool $ignoreEmptyContextAndExtra = false, bool $includeStacktraces = false)
43    {
44        $this->batchMode = $batchMode;
45        $this->appendNewline = $appendNewline;
46        $this->ignoreEmptyContextAndExtra = $ignoreEmptyContextAndExtra;
47        $this->includeStacktraces = $includeStacktraces;
48
49        parent::__construct();
50    }
51
52    /**
53     * The batch mode option configures the formatting style for
54     * multiple records. By default, multiple records will be
55     * formatted as a JSON-encoded array. However, for
56     * compatibility with some API endpoints, alternative styles
57     * are available.
58     */
59    public function getBatchMode(): int
60    {
61        return $this->batchMode;
62    }
63
64    /**
65     * True if newlines are appended to every formatted record
66     */
67    public function isAppendingNewlines(): bool
68    {
69        return $this->appendNewline;
70    }
71
72    /**
73     * {@inheritDoc}
74     */
75    public function format(array $record): string
76    {
77        $normalized = $this->normalize($record);
78
79        if (isset($normalized['context']) && $normalized['context'] === []) {
80            if ($this->ignoreEmptyContextAndExtra) {
81                unset($normalized['context']);
82            } else {
83                $normalized['context'] = new \stdClass;
84            }
85        }
86        if (isset($normalized['extra']) && $normalized['extra'] === []) {
87            if ($this->ignoreEmptyContextAndExtra) {
88                unset($normalized['extra']);
89            } else {
90                $normalized['extra'] = new \stdClass;
91            }
92        }
93
94        return $this->toJson($normalized, true) . ($this->appendNewline ? "\n" : '');
95    }
96
97    /**
98     * {@inheritDoc}
99     */
100    public function formatBatch(array $records): string
101    {
102        switch ($this->batchMode) {
103            case static::BATCH_MODE_NEWLINES:
104                return $this->formatBatchNewlines($records);
105
106            case static::BATCH_MODE_JSON:
107            default:
108                return $this->formatBatchJson($records);
109        }
110    }
111
112    /**
113     * @return self
114     */
115    public function includeStacktraces(bool $include = true): self
116    {
117        $this->includeStacktraces = $include;
118
119        return $this;
120    }
121
122    /**
123     * Return a JSON-encoded array of records.
124     *
125     * @phpstan-param Record[] $records
126     */
127    protected function formatBatchJson(array $records): string
128    {
129        return $this->toJson($this->normalize($records), true);
130    }
131
132    /**
133     * Use new lines to separate records instead of a
134     * JSON-encoded array.
135     *
136     * @phpstan-param Record[] $records
137     */
138    protected function formatBatchNewlines(array $records): string
139    {
140        $instance = $this;
141
142        $oldNewline = $this->appendNewline;
143        $this->appendNewline = false;
144        array_walk($records, function (&$value, $key) use ($instance) {
145            $value = $instance->format($value);
146        });
147        $this->appendNewline = $oldNewline;
148
149        return implode("\n", $records);
150    }
151
152    /**
153     * Normalizes given $data.
154     *
155     * @param mixed $data
156     *
157     * @return mixed
158     */
159    protected function normalize($data, int $depth = 0)
160    {
161        if ($depth > $this->maxNormalizeDepth) {
162            return 'Over '.$this->maxNormalizeDepth.' levels deep, aborting normalization';
163        }
164
165        if (is_array($data)) {
166            $normalized = [];
167
168            $count = 1;
169            foreach ($data as $key => $value) {
170                if ($count++ > $this->maxNormalizeItemCount) {
171                    $normalized['...'] = 'Over '.$this->maxNormalizeItemCount.' items ('.count($data).' total), aborting normalization';
172                    break;
173                }
174
175                $normalized[$key] = $this->normalize($value, $depth + 1);
176            }
177
178            return $normalized;
179        }
180
181        if ($data instanceof \DateTimeInterface) {
182            return $this->formatDate($data);
183        }
184
185        if ($data instanceof Throwable) {
186            return $this->normalizeException($data, $depth);
187        }
188
189        if (is_resource($data)) {
190            return parent::normalize($data);
191        }
192
193        return $data;
194    }
195
196    /**
197     * Normalizes given exception with or without its own stack trace based on
198     * `includeStacktraces` property.
199     *
200     * {@inheritDoc}
201     */
202    protected function normalizeException(Throwable $e, int $depth = 0): array
203    {
204        $data = parent::normalizeException($e, $depth);
205        if (!$this->includeStacktraces) {
206            unset($data['trace']);
207        }
208
209        return $data;
210    }
211}
212