1 | <?php |
||
2 | /** |
||
3 | * @author Todd Burry <[email protected]> |
||
4 | * @copyright 2009-2015 Vanilla Forums Inc. |
||
5 | * @license MIT |
||
6 | */ |
||
7 | |||
8 | namespace Garden\Cli; |
||
9 | |||
10 | use Psr\Log\InvalidArgumentException; |
||
11 | use Psr\Log\LoggerInterface; |
||
12 | use Psr\Log\LoggerTrait; |
||
13 | use Psr\Log\LogLevel; |
||
14 | |||
15 | /** |
||
16 | * A helper class to format CLI output in a log-style format. |
||
17 | */ |
||
18 | class TaskLogger implements LoggerInterface { |
||
19 | use LoggerTrait; |
||
20 | |||
21 | const FIELD_INDENT = '_indent'; |
||
22 | const FIELD_TIME = '_time'; |
||
23 | const FIELD_BEGIN = '_begin'; |
||
24 | const FIELD_END = '_end'; |
||
25 | const FIELD_DURATION = '_duration'; |
||
26 | const FIELD_LEVEL = '_level'; |
||
27 | |||
28 | private static $levels = [ |
||
29 | LogLevel::DEBUG, |
||
30 | LogLevel::INFO, |
||
31 | LogLevel::NOTICE, |
||
32 | LogLevel::WARNING, |
||
33 | LogLevel::ERROR, |
||
34 | LogLevel::CRITICAL, |
||
35 | LogLevel::ALERT, |
||
36 | LogLevel::EMERGENCY, |
||
37 | ]; |
||
38 | /** |
||
39 | * @var int The minimum level deep to output. |
||
40 | */ |
||
41 | private $minLevel = LogLevel::INFO; |
||
42 | /** |
||
43 | * @var array An array of currently running tasks. |
||
44 | */ |
||
45 | private $taskStack = []; |
||
46 | /** |
||
47 | * @var LoggerInterface The logger to ultimately log the information to. |
||
48 | */ |
||
49 | private $logger; |
||
50 | |||
51 | /** |
||
52 | * TaskLogger constructor. |
||
53 | * |
||
54 | * @param LoggerInterface $logger The logger to ultimately log the information to. |
||
55 | * @param string $minLevel The minimum error level that will be logged. One of the **LogLevel** constants. |
||
56 | */ |
||
57 | 36 | public function __construct(LoggerInterface $logger = null, $minLevel = LogLevel::INFO) { |
|
58 | 36 | if ($logger === null) { |
|
59 | $logger = new StreamLogger(); |
||
60 | } |
||
61 | 36 | $this->logger = $logger; |
|
62 | 36 | $this->setMinLevel($minLevel); |
|
63 | 36 | } |
|
64 | |||
65 | /** |
||
66 | * Output a debug message that designates the beginning of a task. |
||
67 | * |
||
68 | * @param string $message The message to output. |
||
69 | * @param array $context Context variables to pass to the message. |
||
70 | * @return $this |
||
71 | */ |
||
72 | 3 | public function beginDebug(string $message, array $context = []) { |
|
73 | 3 | return $this->begin(LogLevel::DEBUG, $message, $context); |
|
74 | } |
||
75 | |||
76 | /** |
||
77 | * Output a message that designates the beginning of a task. |
||
78 | * |
||
79 | * @param string $level |
||
80 | * @param string $message The message to output. |
||
81 | * @param array $context The log context. |
||
82 | * @return $this |
||
83 | */ |
||
84 | 18 | public function begin(string $level, string $message, array $context = []) { |
|
85 | 18 | $output = $this->compareLevel($level, $this->getMinLevel()) >= 0; |
|
86 | 17 | $context = [self::FIELD_BEGIN => true] + $context + [self::FIELD_TIME => microtime(true)]; |
|
87 | 17 | $task = [$level, $message, $context, $output]; |
|
88 | |||
89 | 17 | if ($output) { |
|
90 | 15 | $this->log($level, $message, $context); |
|
91 | } |
||
92 | |||
93 | 17 | array_push($this->taskStack, $task); |
|
94 | |||
95 | 17 | return $this; |
|
96 | } |
||
97 | |||
98 | /** |
||
99 | * Compare two log levels. |
||
100 | * |
||
101 | * @param string $a The first log level to compare. |
||
102 | * @param string $b The second log level to compare. |
||
103 | * @return int Returns -1, 0, or 1. |
||
104 | */ |
||
105 | 35 | private function compareLevel(string $a, string $b): int { |
|
106 | 35 | $i = array_search($a, static::$levels); |
|
0 ignored issues
–
show
Bug
introduced
by
![]() |
|||
107 | 35 | if ($i === false) { |
|
108 | 3 | throw new InvalidArgumentException("Log level is invalid: $a", 500); |
|
109 | } |
||
110 | |||
111 | 32 | return $i <=> array_search($b, static::$levels); |
|
112 | } |
||
113 | |||
114 | /** |
||
115 | * Get the maxLevel. |
||
116 | * |
||
117 | * @return string Returns the maxLevel. |
||
118 | */ |
||
119 | 35 | public function getMinLevel(): string { |
|
120 | 35 | return $this->minLevel; |
|
121 | } |
||
122 | |||
123 | /** |
||
124 | * @param string $minLevel |
||
125 | * @return $this |
||
126 | */ |
||
127 | 36 | public function setMinLevel(string $minLevel) { |
|
128 | 36 | $this->minLevel = $minLevel; |
|
0 ignored issues
–
show
The property
$minLevel was declared of type integer , but $minLevel is of type string . Maybe add a type cast?
This check looks for assignments to scalar types that may be of the wrong type. To ensure the code behaves as expected, it may be a good idea to add an explicit type cast. $answer = 42;
$correct = false;
$correct = (bool) $answer;
![]() |
|||
129 | 36 | return $this; |
|
130 | } |
||
131 | |||
132 | /** |
||
133 | * Logs with an arbitrary level. |
||
134 | * |
||
135 | * @param mixed $level |
||
136 | * @param string $message |
||
137 | * @param array $context |
||
138 | * |
||
139 | * @return void |
||
140 | */ |
||
141 | 32 | public function log($level, $message, array $context = []) { |
|
142 | 32 | if ($this->compareLevel($level, $this->getMinLevel()) >= 0) { |
|
143 | 30 | $this->outputTaskStack(); |
|
144 | |||
145 | // Increase the level begin tasks if this level is higher. |
||
146 | 30 | foreach ($this->taskStack as &$task) { |
|
147 | 4 | if ($this->compareLevel($level, $task[0]) > 0) { |
|
148 | 4 | $task[0] = $level; |
|
149 | } |
||
150 | } |
||
151 | |||
152 | 30 | $this->logInternal($level, $message, [self::FIELD_INDENT => $this->currentIndent()] + $context); |
|
153 | } |
||
154 | 31 | } |
|
155 | |||
156 | /** |
||
157 | * Output the task stack. |
||
158 | */ |
||
159 | 30 | private function outputTaskStack() { |
|
160 | 30 | foreach ($this->taskStack as $indent => &$task) { |
|
161 | 4 | list($taskLevel, $taskMessage, $taskContext, $taskOutput) = $task; |
|
162 | 4 | if (!$taskOutput) { |
|
163 | 2 | $this->logInternal($taskLevel, $taskMessage, [self::FIELD_INDENT => $indent] + $taskContext); |
|
164 | |||
165 | 4 | $task[3] = true; // mark task as outputted |
|
166 | } |
||
167 | } |
||
168 | 30 | } |
|
169 | |||
170 | /** |
||
171 | * Internal log implementation with less error checking. |
||
172 | * |
||
173 | * @param string $level The log level. |
||
174 | * @param string $message The log message. |
||
175 | * @param array $context The log context. |
||
176 | */ |
||
177 | 31 | private function logInternal(string $level, string $message, array $context = []) { |
|
178 | 31 | $context = $context + [self::FIELD_INDENT => $this->currentIndent(), self::FIELD_TIME => microtime(true)]; |
|
179 | 31 | $this->logger->log($level, $message, $context); |
|
180 | 31 | } |
|
181 | |||
182 | /** |
||
183 | * Get the current depth of tasks. |
||
184 | * |
||
185 | * @return int Returns the current level. |
||
186 | */ |
||
187 | 32 | private function currentIndent() { |
|
188 | 32 | return count($this->taskStack); |
|
189 | } |
||
190 | |||
191 | /** |
||
192 | * Output an info message that designates the beginning of a task. |
||
193 | * |
||
194 | * @param string $message The message to output. |
||
195 | * @param array $context Context variables to pass to the message. |
||
196 | * @return $this |
||
197 | */ |
||
198 | 2 | public function beginInfo(string $message, array $context = []) { |
|
199 | 2 | return $this->begin(LogLevel::INFO, $message, $context); |
|
200 | } |
||
201 | |||
202 | /** |
||
203 | * Output a notice message that designates the beginning of a task. |
||
204 | * |
||
205 | * @param string $message The message to output. |
||
206 | * @param array $context Context variables to pass to the message. |
||
207 | * @return $this |
||
208 | */ |
||
209 | 2 | public function beginNotice(string $message, array $context = []) { |
|
210 | 2 | return $this->begin(LogLevel::NOTICE, $message, $context); |
|
211 | } |
||
212 | |||
213 | /** |
||
214 | * Output a warning message that designates the beginning of a task. |
||
215 | * |
||
216 | * @param string $message The message to output. |
||
217 | * @param array $context Context variables to pass to the message. |
||
218 | * @return $this |
||
219 | */ |
||
220 | 1 | public function beginWarning(string $message, array $context = []) { |
|
221 | 1 | return $this->begin(LogLevel::WARNING, $message, $context); |
|
222 | } |
||
223 | |||
224 | /** |
||
225 | * Output an error message that designates the beginning of a task. |
||
226 | * |
||
227 | * @param string $message The message to output. |
||
228 | * @param array $context Context variables to pass to the message. |
||
229 | * @return $this |
||
230 | */ |
||
231 | 1 | public function beginError(string $message, array $context = []) { |
|
232 | 1 | return $this->begin(LogLevel::ERROR, $message, $context); |
|
233 | } |
||
234 | |||
235 | /** |
||
236 | * Output a critical message that designates the beginning of a task. |
||
237 | * |
||
238 | * @param string $message The message to output. |
||
239 | * @param array $context Context variables to pass to the message. |
||
240 | * @return $this |
||
241 | */ |
||
242 | 1 | public function beginCritical(string $message, array $context = []) { |
|
243 | 1 | return $this->begin(LogLevel::CRITICAL, $message, $context); |
|
244 | } |
||
245 | |||
246 | /** |
||
247 | * Output an alert message that designates the beginning of a task. |
||
248 | * |
||
249 | * @param string $message The message to output. |
||
250 | * @param array $context Context variables to pass to the message. |
||
251 | * @return $this |
||
252 | */ |
||
253 | 1 | public function beginAlert(string $message, array $context = []) { |
|
254 | 1 | return $this->begin(LogLevel::ALERT, $message, $context); |
|
255 | } |
||
256 | |||
257 | /** |
||
258 | * Output an emergency message that designates the beginning of a task. |
||
259 | * |
||
260 | * @param string $message The message to output. |
||
261 | * @param array $context Context variables to pass to the message. |
||
262 | * @return $this |
||
263 | */ |
||
264 | 1 | public function beginEmergency(string $message, array $context = []) { |
|
265 | 1 | return $this->begin(LogLevel::EMERGENCY, $message, $context); |
|
266 | } |
||
267 | |||
268 | /** |
||
269 | * Output a message that ends a task with an HTTP status code. |
||
270 | * |
||
271 | * This method is useful if you are making a call to an external API as a task. You can end the task by passing the |
||
272 | * response code to this message. |
||
273 | * |
||
274 | * @param int $httpStatus The HTTP status code that represents the completion of a task. |
||
275 | * @return $this |
||
276 | */ |
||
277 | 4 | public function endHttpStatus(int $httpStatus) { |
|
278 | 4 | $statusStr = sprintf('%03d', $httpStatus); |
|
279 | |||
280 | 4 | if ($httpStatus == 0 || $httpStatus >= 500) { |
|
281 | 2 | $this->end($statusStr, [self::FIELD_LEVEL => LogLevel::CRITICAL]); |
|
282 | 2 | } elseif ($httpStatus >= 400) { |
|
283 | 1 | $this->endError($statusStr); |
|
284 | } else { |
||
285 | 1 | $this->end($statusStr); |
|
286 | } |
||
287 | |||
288 | 4 | return $this; |
|
289 | } |
||
290 | |||
291 | /** |
||
292 | * Output a message that designates a task being completed. |
||
293 | * |
||
294 | * @param string $message The message to output. |
||
295 | * @param array $context Context for the log. There are a few special fields that can be given with the context. |
||
296 | * |
||
297 | * - **TaskLogger::FIELD_TIME**: Specify a specific timestamp for the log. |
||
298 | * - **TaskLogger::FIELD_LEVEL**: Override the level of the end log. Otherwise the level of the beg log is used. |
||
299 | * |
||
300 | * @return $this Returns `$this` for fluent calls. |
||
301 | */ |
||
302 | 11 | public function end(string $message = '', array $context = []) { |
|
303 | 11 | $context = [self::FIELD_INDENT => $this->currentIndent() - 1, self::FIELD_END => true] + $context + [self::FIELD_TIME => microtime(true)]; |
|
304 | 11 | $level = $context[self::FIELD_LEVEL] ?? null; |
|
305 | |||
306 | // Find the task we are finishing. |
||
307 | 11 | $task = end($this->taskStack); |
|
308 | 11 | if ($task !== false) { |
|
309 | 9 | list($taskLevel, $taskMessage, $taskContext, $taskOutput) = $task; |
|
310 | 9 | $context[self::FIELD_DURATION] = $context[self::FIELD_TIME] - $taskContext[self::FIELD_TIME]; |
|
311 | 9 | $level = $level ?: $taskLevel; |
|
312 | } else { |
||
313 | 2 | trigger_error('Called TaskLogger::end() without calling TaskFormatter::begin()', E_USER_NOTICE); |
|
314 | } |
||
315 | 11 | $level = $level ?: LogLevel::INFO; |
|
316 | |||
317 | 11 | $output = $this->compareLevel($level, $this->getMinLevel()) >= 0; |
|
318 | 10 | if ($output) { |
|
319 | 10 | $this->logInternal($level, $message, $context); |
|
320 | } |
||
321 | 10 | array_pop($this->taskStack); |
|
322 | |||
323 | 10 | return $this; |
|
324 | } |
||
325 | |||
326 | /** |
||
327 | * Output a message that represents a task being completed in an error. |
||
328 | * |
||
329 | * When formatting is turned on, error messages are output in red. |
||
330 | * |
||
331 | * @param string $message The message to output. |
||
332 | * @param array $context The log context. |
||
333 | * @return $this |
||
334 | */ |
||
335 | 1 | public function endError(string $message, array $context = []) { |
|
336 | 1 | return $this->end($message, [self::FIELD_LEVEL => LogLevel::ERROR] + $context); |
|
337 | } |
||
338 | } |
||
339 |