Passed
Push — master ( 4562dd...74d0bd )
by Paul
04:12
created

Console::canLogEntry()   A

Complexity

Conditions 3
Paths 3

Size

Total Lines 8
Code Lines 5

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 5
CRAP Score 3.0416

Importance

Changes 0
Metric Value
cc 3
eloc 5
nc 3
nop 2
dl 0
loc 8
ccs 5
cts 6
cp 0.8333
crap 3.0416
rs 10
c 0
b 0
f 0
1
<?php
2
3
namespace GeminiLabs\SiteReviews\Modules;
4
5
use DateTime;
6
use GeminiLabs\SiteReviews\Application;
7
use GeminiLabs\SiteReviews\Helper;
8
use GeminiLabs\SiteReviews\Modules\Session;
9
use ReflectionClass;
10
use Throwable;
11
12
class Console
13
{
14
	const DEBUG_0 = 'debug';         // Detailed debug information
15
	const INFO_1 = 'info';           // Interesting events
16
	const NOTICE_2 = 'notice';       // Normal but significant events
17
	const WARNING_3 = 'warning';     // Exceptional occurrences that are not errors
18
	const ERROR_4 = 'error';         // Runtime errors that do not require immediate action
19
	const CRITICAL_5 = 'critical';   // Critical conditions
20
	const ALERT_6 = 'alert';         // Action must be taken immediately
21
	const EMERGENCY_7 = 'emergency'; // System is unusable
22
23
	protected $file;
24
	protected $log;
25
	protected $onceSessionKey = 'glsr_log_once';
26
27 7
	public function __construct( Application $app )
28
	{
29 7
		$this->file = $app->path( 'console.log' );
30 7
		$this->log = file_exists( $this->file )
31 6
			? file_get_contents( $this->file )
32 1
			: '';
33 7
		$this->reset();
34 7
	}
35
36
	/**
37
	 * @return string
38
	 */
39
	public function __toString()
40
	{
41
		return $this->get();
42
	}
43
44
	/**
45
	 * Action must be taken immediately
46
	 * Example: Entire website down, database unavailable, etc. This should trigger the SMS alerts and wake you up
47
	 * @param mixed $message
48
	 * @param array $context
49
	 * @return static
50
	 */
51
	public function alert( $message, array $context = [] )
52
	{
53
		return $this->log( static::ALERT_6, $message, $context );
54
	}
55
56
	/**
57
	 * @return void
58
	 */
59
	public function clear()
60
	{
61
		$this->log = '';
62
		file_put_contents( $this->file, $this->log );
63
	}
64
65
	/**
66
	 * Critical conditions
67
	 * Example: Application component unavailable, unexpected exception
68
	 * @param mixed $message
69
	 * @param array $context
70
	 * @return static
71
	 */
72
	public function critical( $message, array $context = [] )
73
	{
74
		return $this->log( static::CRITICAL_5, $message, $context );
75
	}
76
77
	/**
78
	 * Detailed debug information
79
	 * @param mixed $message
80
	 * @param array $context
81
	 * @return static
82
	 */
83
	public function debug( $message, array $context = [] )
84
	{
85
		return $this->log( static::DEBUG_0, $message, $context );
86
	}
87
88
	/**
89
	 * System is unusable
90
	 * @param mixed $message
91
	 * @param array $context
92
	 * @return static
93
	 */
94
	public function emergency( $message, array $context = [] )
95
	{
96
		return $this->log( static::EMERGENCY_7, $message, $context );
97
	}
98
99
	/**
100
	 * Runtime errors that do not require immediate action but should typically be logged and monitored
101
	 * @param mixed $message
102
	 * @param array $context
103
	 * @return static
104
	 */
105
	public function error( $message, array $context = [] )
106
	{
107
		return $this->log( static::ERROR_4, $message, $context );
108
	}
109
110
	/**
111
	 * @return string
112
	 */
113
	public function get()
114
	{
115
		return empty( $this->log )
116
			? __( 'Console is empty', 'site-reviews' )
117
			: $this->log;
118
	}
119
120
	/**
121
	 * @return array
122
	 */
123 7
	public function getLevels()
124
	{
125 7
		return array_values( (new ReflectionClass(__CLASS__))->getConstants() );
126
	}
127
128
	/**
129
	 * @param null|string $valueIfEmpty
130
	 * @return string
131
	 */
132
	public function humanSize( $valueIfEmpty = null )
133
	{
134
		$bytes = $this->size();
135
		if( empty( $bytes ) && is_string( $valueIfEmpty )) {
136
			return $valueIfEmpty;
137
		}
138
		$exponent = floor( log( max( $bytes, 1 ), 1024 ));
139
		return round( $bytes / pow( 1024, $exponent ), 2 ).' '.['bytes','KB','MB','GB'][$exponent];
140
	}
141
142
	/**
143
	 * Interesting events
144
	 * Example: User logs in, SQL logs
145
	 * @param mixed $message
146
	 * @param array $context
147
	 * @return static
148
	 */
149 6
	public function info( $message, array $context = [] )
150
	{
151 6
		return $this->log( static::INFO_1, $message, $context );
152
	}
153
154
	/**
155
	 * @param mixed $level
156
	 * @param mixed $message
157
	 * @param array $context
158
	 * @param string $backtraceLine
159
	 * @return static
160
	 */
161 7
	public function log( $level, $message, $context = [], $backtraceLine = '' )
162
	{
163 7
		if( empty( $backtraceLine )) {
164 7
			$backtraceLine = $this->getBacktraceLine();
165
		}
166 7
		if( $this->canLogEntry( $level, $backtraceLine )) {
167 1
			$context = glsr( Helper::class )->consolidateArray( $context );
168 1
			$entry = $this->buildLogEntry(
169 1
				$level,
170 1
				$this->interpolate( $message, $context ),
171 1
				$this->normalizeBacktraceLine( $backtraceLine )
172
			);
173 1
			file_put_contents( $this->file, $entry.PHP_EOL, FILE_APPEND|LOCK_EX );
174 1
			$this->reset();
175
		}
176 7
		return $this;
177
	}
178
179
	/**
180
	 * @return void
181
	 */
182
	public function logOnce()
183
	{
184
		$once = glsr( Session::class )->get( $this->onceSessionKey, [], true );
185
		$once = glsr( Helper::class )->consolidateArray( $once );
186
		$levels = $this->getLevels();
187
		foreach( $once as $entry ) {
188
			if( !in_array( glsr_get( $entry, 'level' ), $levels ))continue;
189
			$level = glsr_get( $entry, 'level' );
190
			$message = glsr_get( $entry, 'message' );
191
			$backtraceLine = glsr_get( $entry, 'backtrace' );
192
			$this->log( $level, $message, [], $backtraceLine );
193
		}
194
	}
195
196
	/**
197
	 * Normal but significant events
198
	 * @param mixed $message
199
	 * @param array $context
200
	 * @return static
201
	 */
202 1
	public function notice( $message, array $context = [] )
203
	{
204 1
		return $this->log( static::NOTICE_2, $message, $context );
205
	}
206
207
	/**
208
	 * @param string $level
209
	 * @param string $handle
210
	 * @param mixed $data
211
	 * @return void
212
	 */
213
	public function once( $level, $handle, $data )
214
	{
215
		$once = glsr( Session::class )->get( $this->onceSessionKey, [] );
216
		$once = glsr( Helper::class )->consolidateArray( $once );
217
		$filtered = array_filter( $once, function( $entry ) use( $level, $handle ) {
218
			return glsr_get( $entry, 'level' ) == $level
219
				&& glsr_get( $entry, 'handle' ) == $handle;
220
		});
221
		if( !empty( $filtered ))return;
222
		$once[] = [
223
			'backtrace' => $this->getBacktraceLineFromData( $data ),
224
			'handle' => $handle,
225
			'level' => $level,
226
			'message' => '[RECURRING] '.$this->getMessageFromData( $data ),
227
		];
228
		glsr( Session::class )->set( $this->onceSessionKey, $once );
229
	}
230
231
	/**
232
	 * @return int
233
	 */
234 7
	public function size()
235
	{
236 7
		return file_exists( $this->file )
237 7
			? filesize( $this->file )
238 7
			: 0;
239
	}
240
241
	/**
242
	 * Exceptional occurrences that are not errors
243
	 * Example: Use of deprecated APIs, poor use of an API, undesirable things that are not necessarily wrong
244
	 * @param mixed $message
245
	 * @param array $context
246
	 * @return static
247
	 */
248
	public function warning( $message, array $context = [] )
249
	{
250
		return $this->log( static::WARNING_3, $message, $context );
251
	}
252
253
	/**
254
	 * @param array $backtrace
255
	 * @param int $index
256
	 * @return string
257
	 */
258 7
	protected function buildBacktraceLine( $backtrace, $index )
259
	{
260 7
		return sprintf( '%s:%s',
261 7
			glsr_get( $backtrace, $index.'.file' ), // realpath
262 7
			glsr_get( $backtrace, $index.'.line' )
263
		);
264
	}
265
266
	/**
267
	 * @param string $level
268
	 * @param mixed $message
269
	 * @param string $backtraceLine
270
	 * @return string
271
	 */
272 1
	protected function buildLogEntry( $level, $message, $backtraceLine = '' )
273
	{
274 1
		if( !empty( $backtraceLine )) {
275 1
			$backtraceLine = '|'.$backtraceLine;
276
		}
277 1
		return sprintf( '[%s%s] %s: %s',
278 1
			current_time( 'mysql' ),
279 1
			$backtraceLine,
280 1
			strtoupper( $level ),
281 1
			$message
282
		);
283
	}
284
285
	/**
286
	 * @param string $level
287
	 * @return bool
288
	 */
289 7
	protected function canLogEntry( $level, $backtraceLine )
290
	{
291 7
		$levelIndex = array_search( $level, $this->getLevels(), true );
292 7
		$result = $levelIndex !== false;
293 7
		if( strpos( $backtraceLine, glsr()->path() ) === false ) {
294
			return $result; // triggered outside of the plugin
295
		}
296 7
		return $result && $levelIndex >= apply_filters( 'site-reviews/console/level', 2 );
297
	}
298
299
	/**
300
	 * @return void|string
301
	 */
302 7
	protected function getBacktraceLine()
303
	{
304 7
		$backtrace = debug_backtrace( DEBUG_BACKTRACE_IGNORE_ARGS, 6 );
305 7
		$search = array_search( 'log', glsr_array_column( $backtrace, 'function' ));
306 7
		if( $search !== false ) {
307 7
			$index = glsr_get( $backtrace, ( $search + 2 ).'.function' ) == '{closure}'
308
				? $search + 4
309 7
				: $search + 1;
310 7
			return $this->buildBacktraceLine( $backtrace, $index );
311
		}
312
		return 'Unknown';
313
	}
314
315
	/**
316
	 * @param mixed $data
317
	 * @return string
318
	 */
319
	protected function getBacktraceLineFromData( $data )
320
	{
321
		$backtrace = $data instanceof Throwable
322
			? $data->getTrace()
323
			: debug_backtrace( DEBUG_BACKTRACE_IGNORE_ARGS, 1 );
324
		return $this->buildBacktraceLine( $backtrace, 0 );
325
	}
326
327
	/**
328
	 * @param mixed $data
329
	 * @return string
330
	 */
331
	protected function getMessageFromData( $data )
332
	{
333
		return $data instanceof Throwable
334
			? $this->normalizeThrowableMessage( $data->getMessage() )
335
			: print_r( $data, 1 );
336
	}
337
338
	/**
339
	 * Interpolates context values into the message placeholders
340
	 * @param mixed $message
341
	 * @param array $context
342
	 * @return string
343
	 */
344 1
	protected function interpolate( $message, $context = [] )
345
	{
346 1
		if( $this->isObjectOrArray( $message ) || !is_array( $context )) {
347
			return print_r( $message, true );
348
		}
349 1
		$replace = [];
350 1
		foreach( $context as $key => $value ) {
351
			$replace['{'.$key.'}'] = $this->normalizeValue( $value );
352
		}
353 1
		return strtr( $message, $replace );
354
	}
355
356
	/**
357
	 * @param mixed $value
358
	 * @return bool
359
	 */
360 1
	protected function isObjectOrArray( $value )
361
	{
362 1
		return is_object( $value ) || is_array( $value );
363
	}
364
365
	/**
366
	 * @param string $backtraceLine
367
	 * @return string
368
	 */
369 1
	protected function normalizeBacktraceLine( $backtraceLine )
370
	{
371
		$search = [
372 1
			glsr()->path( 'plugin/' ),
373 1
			glsr()->path( 'plugin/', false ),
374 1
			trailingslashit( glsr()->path() ),
375 1
			trailingslashit( glsr()->path( '', false )),
376 1
			WP_CONTENT_DIR,
377 1
			ABSPATH
378
		];
379 1
		return str_replace( array_unique( $search ), '', $backtraceLine );
380
	}
381
382
	/**
383
	 * @param string $message
384
	 * @return string
385
	 */
386
	protected function normalizeThrowableMessage( $message )
387
	{
388
		$calledIn = strpos( $message, ', called in' );
389
		return $calledIn !== false
390
			? substr( $message, 0, $calledIn )
391
			: $message;
392
	}
393
394
	/**
395
	 * @param mixed $value
396
	 * @return string
397
	 */
398
	protected function normalizeValue( $value )
399
	{
400
		if( $value instanceof DateTime ) {
401
			$value = $value->format( 'Y-m-d H:i:s' );
402
		}
403
		else if( $this->isObjectOrArray( $value )) {
404
			$value = json_encode( $value );
405
		}
406
		return (string)$value;
407
	}
408
409
	/**
410
	 * @return void
411
	 */
412 7
	protected function reset()
413
	{
414 7
		if( $this->size() <= pow( 1024, 2 ) / 8 )return;
415
		$this->clear();
416
		file_put_contents(
417
			$this->file,
418
			$this->buildLogEntry(
419
				static::INFO_1,
420
				__( 'Console was automatically cleared (128 KB maximum size)', 'site-reviews' )
421
			)
422
		);
423
	}
424
}
425