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\Utils; 15 16/** 17 * Formats incoming records into a one-line string 18 * 19 * This is especially useful for logging to files 20 * 21 * @author Jordi Boggiano <j.boggiano@seld.be> 22 * @author Christophe Coevoet <stof@notk.org> 23 */ 24class LineFormatter extends NormalizerFormatter 25{ 26 public const SIMPLE_FORMAT = "[%datetime%] %channel%.%level_name%: %message% %context% %extra%\n"; 27 28 /** @var string */ 29 protected $format; 30 /** @var bool */ 31 protected $allowInlineLineBreaks; 32 /** @var bool */ 33 protected $ignoreEmptyContextAndExtra; 34 /** @var bool */ 35 protected $includeStacktraces; 36 37 /** 38 * @param string|null $format The format of the message 39 * @param string|null $dateFormat The format of the timestamp: one supported by DateTime::format 40 * @param bool $allowInlineLineBreaks Whether to allow inline line breaks in log entries 41 * @param bool $ignoreEmptyContextAndExtra 42 */ 43 public function __construct(?string $format = null, ?string $dateFormat = null, bool $allowInlineLineBreaks = false, bool $ignoreEmptyContextAndExtra = false, bool $includeStacktraces = false) 44 { 45 $this->format = $format === null ? static::SIMPLE_FORMAT : $format; 46 $this->allowInlineLineBreaks = $allowInlineLineBreaks; 47 $this->ignoreEmptyContextAndExtra = $ignoreEmptyContextAndExtra; 48 $this->includeStacktraces($includeStacktraces); 49 parent::__construct($dateFormat); 50 } 51 52 public function includeStacktraces(bool $include = true): self 53 { 54 $this->includeStacktraces = $include; 55 if ($this->includeStacktraces) { 56 $this->allowInlineLineBreaks = true; 57 } 58 59 return $this; 60 } 61 62 public function allowInlineLineBreaks(bool $allow = true): self 63 { 64 $this->allowInlineLineBreaks = $allow; 65 66 return $this; 67 } 68 69 public function ignoreEmptyContextAndExtra(bool $ignore = true): self 70 { 71 $this->ignoreEmptyContextAndExtra = $ignore; 72 73 return $this; 74 } 75 76 /** 77 * {@inheritDoc} 78 */ 79 public function format(array $record): string 80 { 81 $vars = parent::format($record); 82 83 $output = $this->format; 84 85 foreach ($vars['extra'] as $var => $val) { 86 if (false !== strpos($output, '%extra.'.$var.'%')) { 87 $output = str_replace('%extra.'.$var.'%', $this->stringify($val), $output); 88 unset($vars['extra'][$var]); 89 } 90 } 91 92 foreach ($vars['context'] as $var => $val) { 93 if (false !== strpos($output, '%context.'.$var.'%')) { 94 $output = str_replace('%context.'.$var.'%', $this->stringify($val), $output); 95 unset($vars['context'][$var]); 96 } 97 } 98 99 if ($this->ignoreEmptyContextAndExtra) { 100 if (empty($vars['context'])) { 101 unset($vars['context']); 102 $output = str_replace('%context%', '', $output); 103 } 104 105 if (empty($vars['extra'])) { 106 unset($vars['extra']); 107 $output = str_replace('%extra%', '', $output); 108 } 109 } 110 111 foreach ($vars as $var => $val) { 112 if (false !== strpos($output, '%'.$var.'%')) { 113 $output = str_replace('%'.$var.'%', $this->stringify($val), $output); 114 } 115 } 116 117 // remove leftover %extra.xxx% and %context.xxx% if any 118 if (false !== strpos($output, '%')) { 119 $output = preg_replace('/%(?:extra|context)\..+?%/', '', $output); 120 if (null === $output) { 121 $pcreErrorCode = preg_last_error(); 122 throw new \RuntimeException('Failed to run preg_replace: ' . $pcreErrorCode . ' / ' . Utils::pcreLastErrorMessage($pcreErrorCode)); 123 } 124 } 125 126 return $output; 127 } 128 129 public function formatBatch(array $records): string 130 { 131 $message = ''; 132 foreach ($records as $record) { 133 $message .= $this->format($record); 134 } 135 136 return $message; 137 } 138 139 /** 140 * @param mixed $value 141 */ 142 public function stringify($value): string 143 { 144 return $this->replaceNewlines($this->convertToString($value)); 145 } 146 147 protected function normalizeException(\Throwable $e, int $depth = 0): string 148 { 149 $str = $this->formatException($e); 150 151 if ($previous = $e->getPrevious()) { 152 do { 153 $str .= "\n[previous exception] " . $this->formatException($previous); 154 } while ($previous = $previous->getPrevious()); 155 } 156 157 return $str; 158 } 159 160 /** 161 * @param mixed $data 162 */ 163 protected function convertToString($data): string 164 { 165 if (null === $data || is_bool($data)) { 166 return var_export($data, true); 167 } 168 169 if (is_scalar($data)) { 170 return (string) $data; 171 } 172 173 return $this->toJson($data, true); 174 } 175 176 protected function replaceNewlines(string $str): string 177 { 178 if ($this->allowInlineLineBreaks) { 179 if (0 === strpos($str, '{')) { 180 return str_replace(array('\r', '\n'), array("\r", "\n"), $str); 181 } 182 183 return $str; 184 } 185 186 return str_replace(["\r\n", "\r", "\n"], ' ', $str); 187 } 188 189 private function formatException(\Throwable $e): string 190 { 191 $str = '[object] (' . Utils::getClass($e) . '(code: ' . $e->getCode(); 192 if ($e instanceof \SoapFault) { 193 if (isset($e->faultcode)) { 194 $str .= ' faultcode: ' . $e->faultcode; 195 } 196 197 if (isset($e->faultactor)) { 198 $str .= ' faultactor: ' . $e->faultactor; 199 } 200 201 if (isset($e->detail)) { 202 if (is_string($e->detail)) { 203 $str .= ' detail: ' . $e->detail; 204 } elseif (is_object($e->detail) || is_array($e->detail)) { 205 $str .= ' detail: ' . $this->toJson($e->detail, true); 206 } 207 } 208 } 209 $str .= '): ' . $e->getMessage() . ' at ' . $e->getFile() . ':' . $e->getLine() . ')'; 210 211 if ($this->includeStacktraces) { 212 $str .= "\n[stacktrace]\n" . $e->getTraceAsString() . "\n"; 213 } 214 215 return $str; 216 } 217} 218