1: <?php
2:
3: 4: 5: 6:
7:
8: namespace Tracy;
9:
10:
11: 12: 13:
14: class Logger implements ILogger
15: {
16:
17: public $directory;
18:
19:
20: public $email;
21:
22:
23: public $fromEmail;
24:
25:
26: public $emailSnooze = '2 days';
27:
28:
29: public $mailer;
30:
31:
32: private $blueScreen;
33:
34:
35: 36: 37: 38:
39: public function __construct($directory, $email = null, BlueScreen $blueScreen = null)
40: {
41: $this->directory = $directory;
42: $this->email = $email;
43: $this->blueScreen = $blueScreen;
44: $this->mailer = [$this, 'defaultMailer'];
45: }
46:
47:
48: 49: 50: 51: 52: 53:
54: public function log($message, $priority = self::INFO)
55: {
56: if (!$this->directory) {
57: throw new \LogicException('Logging directory is not specified.');
58: } elseif (!is_dir($this->directory)) {
59: throw new \RuntimeException("Logging directory '$this->directory' is not found or is not directory.");
60: }
61:
62: $exceptionFile = $message instanceof \Exception || $message instanceof \Throwable
63: ? $this->getExceptionFile($message)
64: : null;
65: $line = static::formatLogLine($message, $exceptionFile);
66: $file = $this->directory . '/' . strtolower($priority ?: self::INFO) . '.log';
67:
68: if (!@file_put_contents($file, $line . PHP_EOL, FILE_APPEND | LOCK_EX)) {
69: throw new \RuntimeException("Unable to write to log file '$file'. Is directory writable?");
70: }
71:
72: if ($exceptionFile) {
73: $this->logException($message, $exceptionFile);
74: }
75:
76: if (in_array($priority, [self::ERROR, self::EXCEPTION, self::CRITICAL], true)) {
77: $this->sendEmail($message);
78: }
79:
80: return $exceptionFile;
81: }
82:
83:
84: 85: 86: 87:
88: public static function formatMessage($message)
89: {
90: if ($message instanceof \Exception || $message instanceof \Throwable) {
91: while ($message) {
92: $tmp[] = ($message instanceof \ErrorException
93: ? Helpers::errorTypeToString($message->getSeverity()) . ': ' . $message->getMessage()
94: : Helpers::getClass($message) . ': ' . $message->getMessage() . ($message->getCode() ? ' #' . $message->getCode() : '')
95: ) . ' in ' . $message->getFile() . ':' . $message->getLine();
96: $message = $message->getPrevious();
97: }
98: $message = implode("\ncaused by ", $tmp);
99:
100: } elseif (!is_string($message)) {
101: $message = Dumper::toText($message);
102: }
103:
104: return trim($message);
105: }
106:
107:
108: 109: 110: 111:
112: public static function formatLogLine($message, $exceptionFile = null)
113: {
114: return implode(' ', [
115: @date('[Y-m-d H-i-s]'),
116: preg_replace('#\s*\r?\n\s*#', ' ', static::formatMessage($message)),
117: ' @ ' . Helpers::getSource(),
118: $exceptionFile ? ' @@ ' . basename($exceptionFile) : null,
119: ]);
120: }
121:
122:
123: 124: 125: 126:
127: public function getExceptionFile($exception)
128: {
129: while ($exception) {
130: $data[] = [
131: get_class($exception), $exception->getMessage(), $exception->getCode(), $exception->getFile(), $exception->getLine(),
132: array_map(function ($item) { unset($item['args']); return $item; }, $exception->getTrace()),
133: ];
134: $exception = $exception->getPrevious();
135: }
136: $hash = substr(md5(serialize($data)), 0, 10);
137: $dir = strtr($this->directory . '/', '\\/', DIRECTORY_SEPARATOR . DIRECTORY_SEPARATOR);
138: foreach (new \DirectoryIterator($this->directory) as $file) {
139: if (strpos($file->getBasename(), $hash)) {
140: return $dir . $file;
141: }
142: }
143: return $dir . 'exception--' . @date('Y-m-d--H-i') . "--$hash.html";
144: }
145:
146:
147: 148: 149: 150: 151:
152: protected function logException($exception, $file = null)
153: {
154: $file = $file ?: $this->getExceptionFile($exception);
155: $bs = $this->blueScreen ?: new BlueScreen;
156: $bs->renderToFile($exception, $file);
157: return $file;
158: }
159:
160:
161: 162: 163: 164:
165: protected function sendEmail($message)
166: {
167: $snooze = is_numeric($this->emailSnooze)
168: ? $this->emailSnooze
169: : @strtotime($this->emailSnooze) - time();
170:
171: if (
172: $this->email
173: && $this->mailer
174: && @filemtime($this->directory . '/email-sent') + $snooze < time()
175: && @file_put_contents($this->directory . '/email-sent', 'sent')
176: ) {
177: call_user_func($this->mailer, $message, implode(', ', (array) $this->email));
178: }
179: }
180:
181:
182: 183: 184: 185: 186: 187: 188:
189: public function defaultMailer($message, $email)
190: {
191: $host = preg_replace('#[^\w.-]+#', '', isset($_SERVER['HTTP_HOST']) ? $_SERVER['HTTP_HOST'] : php_uname('n'));
192: $parts = str_replace(
193: ["\r\n", "\n"],
194: ["\n", PHP_EOL],
195: [
196: 'headers' => implode("\n", [
197: 'From: ' . ($this->fromEmail ?: "noreply@$host"),
198: 'X-Mailer: Tracy',
199: 'Content-Type: text/plain; charset=UTF-8',
200: 'Content-Transfer-Encoding: 8bit',
201: ]) . "\n",
202: 'subject' => "PHP: An error occurred on the server $host",
203: 'body' => static::formatMessage($message) . "\n\nsource: " . Helpers::getSource(),
204: ]
205: );
206:
207: mail($email, $parts['subject'], $parts['body'], $parts['headers']);
208: }
209: }
210: