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