Update copyright
[friendica.git/.git] / src / Util / Logger / SyslogLogger.php
1 <?php
2 /**
3  * @copyright Copyright (C) 2010-2021, the Friendica project
4  *
5  * @license GNU AGPL version 3 or any later version
6  *
7  * This program is free software: you can redistribute it and/or modify
8  * it under the terms of the GNU Affero General Public License as
9  * published by the Free Software Foundation, either version 3 of the
10  * License, or (at your option) any later version.
11  *
12  * This program is distributed in the hope that it will be useful,
13  * but WITHOUT ANY WARRANTY; without even the implied warranty of
14  * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
15  * GNU Affero General Public License for more details.
16  *
17  * You should have received a copy of the GNU Affero General Public License
18  * along with this program.  If not, see <https://www.gnu.org/licenses/>.
19  *
20  */
21
22 namespace Friendica\Util\Logger;
23
24 use Friendica\Network\HTTPException\InternalServerErrorException;
25 use Friendica\Util\Introspection;
26 use Psr\Log\LogLevel;
27
28 /**
29  * A Logger instance for syslogging (fast, but simple)
30  * @see http://php.net/manual/en/function.syslog.php
31  */
32 class SyslogLogger extends AbstractLogger
33 {
34         const IDENT = 'Friendica';
35
36         /**
37          * Translates LogLevel log levels to syslog log priorities.
38          * @var array
39          */
40         private $logLevels = [
41                 LogLevel::DEBUG     => LOG_DEBUG,
42                 LogLevel::INFO      => LOG_INFO,
43                 LogLevel::NOTICE    => LOG_NOTICE,
44                 LogLevel::WARNING   => LOG_WARNING,
45                 LogLevel::ERROR     => LOG_ERR,
46                 LogLevel::CRITICAL  => LOG_CRIT,
47                 LogLevel::ALERT     => LOG_ALERT,
48                 LogLevel::EMERGENCY => LOG_EMERG,
49         ];
50
51         /**
52          * Translates log priorities to string outputs
53          * @var array
54          */
55         private $logToString = [
56                 LOG_DEBUG   => 'DEBUG',
57                 LOG_INFO    => 'INFO',
58                 LOG_NOTICE  => 'NOTICE',
59                 LOG_WARNING => 'WARNING',
60                 LOG_ERR     => 'ERROR',
61                 LOG_CRIT    => 'CRITICAL',
62                 LOG_ALERT   => 'ALERT',
63                 LOG_EMERG   => 'EMERGENCY'
64         ];
65
66         /**
67          * Indicates what logging options will be used when generating a log message
68          * @see http://php.net/manual/en/function.openlog.php#refsect1-function.openlog-parameters
69          *
70          * @var int
71          */
72         private $logOpts;
73
74         /**
75          * Used to specify what type of program is logging the message
76          * @see http://php.net/manual/en/function.openlog.php#refsect1-function.openlog-parameters
77          *
78          * @var int
79          */
80         private $logFacility;
81
82         /**
83          * The minimum loglevel at which this logger will be triggered
84          * @var int
85          */
86         private $logLevel;
87
88         /**
89          * A error message of the current operation
90          * @var string
91          */
92         private $errorMessage;
93
94         /**
95          * {@inheritdoc}
96          * @param string $level       The minimum loglevel at which this logger will be triggered
97          * @param int    $logOpts     Indicates what logging options will be used when generating a log message
98          * @param int    $logFacility Used to specify what type of program is logging the message
99          *
100          * @throws \Exception
101          */
102         public function __construct($channel, Introspection $introspection, $level = LogLevel::NOTICE, $logOpts = LOG_PID, $logFacility = LOG_USER)
103         {
104                 parent::__construct($channel, $introspection);
105                 $this->logOpts = $logOpts;
106                 $this->logFacility = $logFacility;
107                 $this->logLevel = $this->mapLevelToPriority($level);
108                 $this->introspection->addClasses(array(self::class));
109         }
110
111         /**
112          * Adds a new entry to the syslog
113          *
114          * @param int    $level
115          * @param string $message
116          * @param array  $context
117          *
118          * @throws InternalServerErrorException if the syslog isn't available
119          */
120         protected function addEntry($level, $message, $context = [])
121         {
122                 $logLevel = $this->mapLevelToPriority($level);
123
124                 if ($logLevel > $this->logLevel) {
125                         return;
126                 }
127
128                 $formattedLog = $this->formatLog($logLevel, $message, $context);
129                 $this->write($logLevel, $formattedLog);
130         }
131
132         /**
133          * Maps the LogLevel (@see LogLevel) to a SysLog priority (@see http://php.net/manual/en/function.syslog.php#refsect1-function.syslog-parameters)
134          *
135          * @param string $level A LogLevel
136          *
137          * @return int The SysLog priority
138          *
139          * @throws \Psr\Log\InvalidArgumentException If the loglevel isn't valid
140          */
141         public function mapLevelToPriority($level)
142         {
143                 if (!array_key_exists($level, $this->logLevels)) {
144                         throw new \InvalidArgumentException(sprintf('The level "%s" is not valid.', $level));
145                 }
146
147                 return $this->logLevels[$level];
148         }
149
150         /**
151          * Closes the Syslog
152          */
153         public function close()
154         {
155                 closelog();
156         }
157
158         /**
159          * Writes a message to the syslog
160          * @see http://php.net/manual/en/function.syslog.php#refsect1-function.syslog-parameters
161          *
162          * @param int    $priority The Priority
163          * @param string $message  The message of the log
164          *
165          * @throws InternalServerErrorException if syslog cannot be used
166          */
167         private function write($priority, $message)
168         {
169                 set_error_handler([$this, 'customErrorHandler']);
170                 $opened = openlog(self::IDENT, $this->logOpts, $this->logFacility);
171                 restore_error_handler();
172
173                 if (!$opened) {
174                         throw new \UnexpectedValueException(sprintf('Can\'t open syslog for ident "%s" and facility "%s": ' . $this->errorMessage, $this->channel, $this->logFacility));
175                 }
176
177                 $this->syslogWrapper($priority, $message);
178         }
179
180         /**
181          * Formats a log record for the syslog output
182          *
183          * @param int    $level   The loglevel/priority
184          * @param string $message The message
185          * @param array  $context The context of this call
186          *
187          * @return string the formatted syslog output
188          */
189         private function formatLog($level, $message, $context = [])
190         {
191                 $record = $this->introspection->getRecord();
192                 $record = array_merge($record, ['uid' => $this->logUid]);
193                 $logMessage = '';
194
195                 $logMessage .= $this->channel . ' ';
196                 $logMessage .= '[' . $this->logToString[$level] . ']: ';
197                 $logMessage .= $this->psrInterpolate($message, $context) . ' ';
198                 $logMessage .= @json_encode($context) . ' - ';
199                 $logMessage .= @json_encode($record);
200
201                 return $logMessage;
202         }
203
204         private function customErrorHandler($code, $msg)
205         {
206                 $this->errorMessage = preg_replace('{^(fopen|mkdir)\(.*?\): }', '', $msg);
207         }
208
209         /**
210          * A syslog wrapper to make syslog functionality testable
211          *
212          * @param int    $level The syslog priority
213          * @param string $entry The message to send to the syslog function
214          */
215         protected function syslogWrapper($level, $entry)
216         {
217                 set_error_handler([$this, 'customErrorHandler']);
218                 $written = syslog($level, $entry);
219                 restore_error_handler();
220
221                 if (!$written) {
222                         throw new \UnexpectedValueException(sprintf('Can\'t write into syslog for ident "%s" and facility "%s": ' . $this->errorMessage, $this->channel, $this->logFacility));
223                 }
224         }
225 }