5cb1f8c9e69b13a4eca3016d69099deca7f54f09
[friendica.git/.git] / src / Util / Logger / SyslogLogger.php
1 <?php
2
3 namespace Friendica\Util\Logger;
4
5 use Friendica\Network\HTTPException\InternalServerErrorException;
6 use Friendica\Util\Introspection;
7 use Friendica\Util\Strings;
8 use Psr\Log\InvalidArgumentException;
9 use Psr\Log\LoggerInterface;
10 use Psr\Log\LogLevel;
11
12 /**
13  * A Logger instance for syslogging (fast, but simple)
14  * @see http://php.net/manual/en/function.syslog.php
15  */
16 class SyslogLogger implements LoggerInterface
17 {
18         const IDENT = 'Friendica';
19
20         /**
21          * Translates LogLevel log levels to syslog log priorities.
22          * @var array
23          */
24         private $logLevels = [
25                 LogLevel::DEBUG     => LOG_DEBUG,
26                 LogLevel::INFO      => LOG_INFO,
27                 LogLevel::NOTICE    => LOG_NOTICE,
28                 LogLevel::WARNING   => LOG_WARNING,
29                 LogLevel::ERROR     => LOG_ERR,
30                 LogLevel::CRITICAL  => LOG_CRIT,
31                 LogLevel::ALERT     => LOG_ALERT,
32                 LogLevel::EMERGENCY => LOG_EMERG,
33         ];
34
35         /**
36          * Translates log priorities to string outputs
37          * @var array
38          */
39         private $logToString = [
40                 LOG_DEBUG   => 'DEBUG',
41                 LOG_INFO    => 'INFO',
42                 LOG_NOTICE  => 'NOTICE',
43                 LOG_WARNING => 'WARNING',
44                 LOG_ERR     => 'ERROR',
45                 LOG_CRIT    => 'CRITICAL',
46                 LOG_ALERT   => 'ALERT',
47                 LOG_EMERG   => 'EMERGENCY'
48         ];
49
50         /**
51          * The channel of the current process (added to each message)
52          * @var string
53          */
54         private $channel;
55
56         /**
57          * Indicates what logging options will be used when generating a log message
58          * @see http://php.net/manual/en/function.openlog.php#refsect1-function.openlog-parameters
59          *
60          * @var int
61          */
62         private $logOpts;
63
64         /**
65          * Used to specify what type of program is logging the message
66          * @see http://php.net/manual/en/function.openlog.php#refsect1-function.openlog-parameters
67          *
68          * @var int
69          */
70         private $logFacility;
71
72         /**
73          * The minimum loglevel at which this logger will be triggered
74          * @var int
75          */
76         private $logLevel;
77
78         /**
79          * The Introspection for the current call
80          * @var Introspection
81          */
82         private $introspection;
83
84         /**
85          * The UID of the current call
86          * @var string
87          */
88         private $logUid;
89
90         /**
91          * @param string        $channel       The output channel
92          * @param Introspection $introspection The introspection of the current call
93          * @param string        $level         The minimum loglevel at which this logger will be triggered
94          * @param int           $logOpts       Indicates what logging options will be used when generating a log message
95          * @param int           $logFacility   Used to specify what type of program is logging the message
96          *
97          * @throws \Exception
98          */
99         public function __construct($channel, Introspection $introspection, $level = LogLevel::NOTICE, $logOpts = LOG_PID, $logFacility = LOG_USER)
100         {
101                 $this->logUid = Strings::getRandomHex(6);
102                 $this->channel = $channel;
103                 $this->logOpts = $logOpts;
104                 $this->logFacility = $logFacility;
105                 $this->logLevel = $this->mapLevelToPriority($level);
106                 $this->introspection = $introspection;
107         }
108
109         /**
110          * Maps the LogLevel (@see LogLevel ) to a SysLog priority (@see http://php.net/manual/en/function.syslog.php#refsect1-function.syslog-parameters )
111          *
112          * @param string $level A LogLevel
113          *
114          * @return int The SysLog priority
115          *
116          * @throws \Psr\Log\InvalidArgumentException If the loglevel isn't valid
117          */
118         public function mapLevelToPriority($level)
119         {
120                 if (!array_key_exists($level, $this->logLevels)) {
121                         throw new InvalidArgumentException('LogLevel \'' . $level . '\' isn\'t valid.');
122                 }
123
124                 return $this->logLevels[$level];
125         }
126
127         /**
128          * Writes a message to the syslog
129          * @see http://php.net/manual/en/function.syslog.php#refsect1-function.syslog-parameters
130          *
131          * @param int    $priority The Priority
132          * @param string $message  The message of the log
133          *
134          * @throws InternalServerErrorException if syslog cannot be used
135          */
136         private function write($priority, $message)
137         {
138                 if (!openlog(self::IDENT, $this->logOpts, $this->logFacility)) {
139                         throw new InternalServerErrorException('Can\'t open syslog for ident "' . $this->channel . '" and facility "' . $this->logFacility . '""');
140                 }
141
142                 syslog($priority, $message);
143         }
144
145         /**
146          * Closes the Syslog
147          */
148         public function close()
149         {
150                 closelog();
151         }
152
153         /**
154          * Formats a log record for the syslog output
155          *
156          * @param int    $level   The loglevel/priority
157          * @param string $message The message
158          * @param array  $context The context of this call
159          *
160          * @return string the formatted syslog output
161          */
162         private function formatLog($level, $message, $context = [])
163         {
164                 $record = $this->introspection->getRecord();
165                 $record = array_merge($record, ['uid' => $this->logUid]);
166                 $logMessage = '';
167
168                 $logMessage .= $this->channel . ' ';
169                 $logMessage .= '[' . $this->logToString[$level] . ']: ';
170                 $logMessage .= $this->psrInterpolate($message, $context) . ' ';
171                 $logMessage .= @json_encode($context) . ' - ';
172                 $logMessage .= @json_encode($record);
173
174                 return $logMessage;
175         }
176
177         /**
178          * Simple interpolation of PSR-3 compliant replacements ( variables between '{' and '}' )
179          * @see https://www.php-fig.org/psr/psr-3/#12-message
180          *
181          * @param string $message
182          * @param array  $context
183          *
184          * @return string the interpolated message
185          */
186         private function psrInterpolate($message, array $context = array())
187         {
188                 $replace = [];
189                 foreach ($context as $key => $value) {
190                         // check that the value can be casted to string
191                         if (!is_array($value) && (!is_object($value) || method_exists($value, '__toString'))) {
192                                 $replace['{' . $key . '}'] = $value;
193                         } elseif (is_array($value)) {
194                                 $replace['{' . $key . '}'] = @json_encode($value);
195                         }
196                 }
197
198                 return strtr($message, $replace);
199         }
200
201         /**
202          * Adds a new entry to the syslog
203          *
204          * @param int    $level
205          * @param string $message
206          * @param array  $context
207          *
208          * @throws InternalServerErrorException if the syslog isn't available
209          */
210         private function addEntry($level, $message, $context = [])
211         {
212                 if ($level >= $this->logLevel) {
213                         return;
214                 }
215
216                 $formattedLog = $this->formatLog($level, $message, $context);
217                 $this->write($level, $formattedLog);
218         }
219
220         /**
221          * {@inheritdoc}
222          * @throws InternalServerErrorException if the syslog isn't available
223          */
224         public function emergency($message, array $context = array())
225         {
226                 $this->addEntry(LOG_EMERG, $message, $context);
227         }
228
229         /**
230          * {@inheritdoc}
231          * @throws InternalServerErrorException if the syslog isn't available
232          */
233         public function alert($message, array $context = array())
234         {
235                 $this->addEntry(LOG_ALERT, $message, $context);
236         }
237
238         /**
239          * {@inheritdoc}
240          * @throws InternalServerErrorException if the syslog isn't available
241          */
242         public function critical($message, array $context = array())
243         {
244                 $this->addEntry(LOG_CRIT, $message, $context);
245         }
246
247         /**
248          * {@inheritdoc}
249          * @throws InternalServerErrorException if the syslog isn't available
250          */
251         public function error($message, array $context = array())
252         {
253                 $this->addEntry(LOG_ERR, $message, $context);
254         }
255
256         /**
257          * {@inheritdoc}
258          * @throws InternalServerErrorException if the syslog isn't available
259          */
260         public function warning($message, array $context = array())
261         {
262                 $this->addEntry(LOG_WARNING, $message, $context);
263         }
264
265         /**
266          * {@inheritdoc}
267          * @throws InternalServerErrorException if the syslog isn't available
268          */
269         public function notice($message, array $context = array())
270         {
271                 $this->addEntry(LOG_NOTICE, $message, $context);
272         }
273
274         /**
275          * {@inheritdoc}
276          * @throws InternalServerErrorException if the syslog isn't available
277          */
278         public function info($message, array $context = array())
279         {
280                 $this->addEntry(LOG_INFO, $message, $context);
281         }
282
283         /**
284          * {@inheritdoc}
285          * @throws InternalServerErrorException if the syslog isn't available
286          */
287         public function debug($message, array $context = array())
288         {
289                 $this->addEntry(LOG_DEBUG, $message, $context);
290         }
291
292         /**
293          * {@inheritdoc}
294          * @throws InternalServerErrorException if the syslog isn't available
295          */
296         public function log($level, $message, array $context = array())
297         {
298                 $logLevel = $this->mapLevelToPriority($level);
299                 $this->addEntry($logLevel, $message, $context);
300         }
301 }