PluginProbe
DecaLog / 4.4.0
DecaLog v4.4.0
3.0.2 3.1.0 3.10.0 3.2.0 3.3.0 3.4.0 3.4.1 3.5.0 3.5.1 3.6.0 3.6.1 3.6.2 3.6.3 3.7.0 3.7.1 3.8.0 3.9.0 3.9.1 4.0.0 4.1.0 4.2.0 4.3.0 4.3.1 4.4.0 4.5.0 All 75 releases
decalog / includes / features / class-traceviewer.php

class-traceviewer.php in DecaLog 4.4.0, at includes/features/class-traceviewer.php

546 lines 18.2 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 <?php
2 /**
3 * Trace viewer
4 *
5 * Handles a view for a specific trace.
6 *
7 * @package Features
8 * @author Pierre Lannoy <https://pierre.lannoy.fr/>.
9 * @since 3.0.0
10 */
11
12 namespace Decalog\Plugin\Feature;
13
14 use Decalog\Storage\AbstractStorage;
15 use Decalog\Storage\APCuStorage;
16 use Decalog\Storage\DBTraceStorage;
17 use Decalog\System\Date;
18 use Decalog\System\Option;
19 use Decalog\System\Timezone;
20 use Feather;
21 use Feather\Icons;
22 use Decalog\System\User;
23 use Decalog\Plugin\Feature\Traces;
24
25 /**
26 * Define the trace viewer functionality.
27 *
28 * Handles a view for a specific trace.
29 *
30 * @package Features
31 * @author Pierre Lannoy <https://pierre.lannoy.fr/>.
32 * @since 3.0.0
33 */
34 class TraceViewer {
35
36 /**
37 * The internal logger.
38 *
39 * @since 3.0.0
40 * @access protected
41 * @var DLogger $logger The plugin admin logger.
42 */
43 protected $logger;
44
45 /**
46 * The screen id.
47 *
48 * @since 3.0.0
49 * @var string $screen_id The screen id.
50 */
51 private static $screen_id = 'decalog_trace_viewer';
52
53 /**
54 * The full trace detail.
55 *
56 * @since 3.0.0
57 * @var array $trace The full trace detail.
58 */
59 private $trace = null;
60
61 /**
62 * The traces log id.
63 *
64 * @since 3.0.0
65 * @var array $logid The traces log id.
66 */
67 private $logid = null;
68
69 /**
70 * The trace id.
71 *
72 * @since 3.0.0
73 * @var array $traceid The trace id.
74 */
75 private $traceid = null;
76
77 /**
78 * Colors for timeline.
79 *
80 * @since 1.0.0
81 * @var array $colors The colors array.
82 */
83 private $colors = [
84 'main request' => '#73879C',
85 'server' => '#73879C',
86 'wordpress' => '#73879C',
87 'core' => '#3398DB',
88 'plugin' => '#9B59B6',
89 'theme' => '#b2c326',
90 'db' => '#BDC3C6',
91 ];
92
93 /**
94 * Initialize the class and set its properties.
95 *
96 * @param string $logid The events log id.
97 * @param string $traceid The specific trace id.
98 * @param DLogger $logger The internal logger.
99 * @since 3.0.0
100 */
101 public function __construct( $logid, $traceid, $logger ) {
102 $this->logid = $logid;
103 $this->logger = $logger;
104 $this->traceid = $traceid;
105 $this->trace = null;
106 $log = null;
107 $loggers = Option::network_get( 'loggers' );
108 if ( array_key_exists( $this->logid, $loggers ) ) {
109 $bucket_name = 'decalog_' . str_replace( '-', '', $this->logid );
110 switch ( $loggers[ $this->logid ]['configuration']['constant-storage'] ) {
111 case 'apcu':
112 $storage = new APCuStorage( $bucket_name );
113 break;
114 default:
115 $storage = new DBTraceStorage( $bucket_name );
116 }
117 $log = $storage->get_by_id( $this->traceid );
118 }
119 if ( isset( $log ) ) {
120 foreach ( Traces::get() as $logged ) {
121 if ( $logged['id'] === $this->logid ) {
122 if ( ! array_key_exists( 'limit', $logged ) || in_array( $log['site_id'], $logged ['limit'] ) ) {
123 $this->trace = $log;
124 $this->trace['spans'] = json_decode( $this->trace['spans'], true );
125 break;
126 }
127 }
128 }
129 }
130 wp_enqueue_script( 'postbox' );
131 }
132
133 /**
134 * Append custom panel HTML to the "Screen Options" box of the current page.
135 * Callback for the 'screen_settings' filter.
136 *
137 * @param string $current Current content.
138 * @param object $screen The current screen.
139 * @return string The HTML code to append to "Screen Options".
140 * @since 3.0.0
141 */
142 public function display_screen_settings( $current, $screen ) {
143 if ( is_object( $screen ) && false !== strpos( $screen->id, 'page_decalog-tviewer' ) && false === strpos( $current, 'id="option-decalog-tviewer"' ) ) {
144 $current .= '<div id="option-decalog-tviewer" class="metabox-prefs custom-options-panel requires-autosave"><input type="hidden" name="_wpnonce-decalog_tviewer" value="' . wp_create_nonce( 'save_settings_decalog_tviewer' ) . '" />';
145 $current .= $this->get_options();
146 $current .= '</div>';
147 }
148 return $current;
149 }
150
151 /**
152 * Add options.
153 *
154 * @since 3.0.0
155 */
156 public function add_metaboxes_options() {
157 $this->add_metaboxes();
158 }
159
160 /**
161 * Get the box options.
162 *
163 * @return string The HTML code to append.
164 * @since 3.0.0
165 */
166 public function get_options() {
167 $result = '<fieldset class="metabox-prefs">';
168 $result .= '<legend>' . decalog_esc_html__( 'Boxes', 'decalog' ) . '</legend>';
169 $result .= $this->meta_box_prefs();
170 $result .= '</fieldset>';
171 return $result;
172 }
173
174 /**
175 * Prints the meta box preferences.
176 *
177 * @return string The HTML code to append.
178 * @since 3.0.0
179 */
180 public function meta_box_prefs() {
181 global $wp_meta_boxes;
182 $result = '';
183 if ( empty( $wp_meta_boxes[ self::$screen_id ] ) ) {
184 return '';
185 }
186 $hidden = get_hidden_meta_boxes( self::$screen_id );
187 foreach ( array_keys( $wp_meta_boxes[ self::$screen_id ] ) as $context ) {
188 foreach ( [ 'high', 'core', 'default', 'low' ] as $priority ) {
189 if ( ! isset( $wp_meta_boxes[ self::$screen_id ][ $context ][ $priority ] ) ) {
190 continue;
191 }
192 foreach ( $wp_meta_boxes[ self::$screen_id ][ $context ][ $priority ] as $box ) {
193 if ( false === $box || ! $box['title'] ) {
194 continue;
195 }
196 if ( 'submitdiv' === $box['id'] || 'linksubmitdiv' === $box['id'] ) {
197 continue;
198 }
199 $box_id = $box['id'];
200 $result .= '<label for="' . $box_id . '-hide">';
201 $result .= '<input class="hide-postbox-tog" name="' . $box_id . '-hide" type="checkbox" id="' . $box_id . '-hide" value="' . $box_id . '"' . ( ! in_array( $box_id, $hidden, false ) ? ' checked="checked"' : '' ) . ' />';
202 $result .= $box['title'] . '</label>';
203 }
204 }
205 }
206 return $result;
207 }
208
209 /**
210 * Get the event viewer.
211 *
212 * @since 3.0.0
213 **/
214 public function get() {
215 echo '<div class="wrap">';
216 if ( isset( $this->trace ) ) {
217 $icon = '<img style="width:30px;float:left;padding-right:8px;" src="' . Icons::get_base64( 'clock', '#ABCFF9', '#192783' ) . '" />';
218 $name = ChannelTypes::$channel_names[ strtoupper( $this->trace['channel'] ) ] . '&nbsp;#' . $this->trace['id'];
219 // phpcs:ignore
220 echo '<h2>' . $icon . $name . '</h2>';
221 settings_errors();
222 echo '<form name="decalog_trace" method="post">';
223 echo '<div id="dashboard-full-wrap">';
224 echo ' <div id="dashboard-full" class="metabox-holder">';
225 echo ' <div id="postbox-container-0" class="postbox-container" style="width:100%;margin-bottom: -10px;">';
226 do_meta_boxes( self::$screen_id, 'full', null );
227 echo ' </div>';
228 echo ' </div>';
229 echo '</div>';
230 echo '<div id="dashboard-widgets-wrap">';
231 wp_nonce_field( 'closedpostboxes', 'closedpostboxesnonce', false );
232 wp_nonce_field( 'meta-box-order', 'meta-box-order-nonce', false );
233 echo ' <div id="dashboard-widgets" class="metabox-holder">';
234 echo ' <div id="postbox-container-1" class="postbox-container">';
235 do_meta_boxes( self::$screen_id, 'advanced', null );
236 echo ' </div>';
237 echo ' <div id="postbox-container-2" class="postbox-container">';
238 do_meta_boxes( self::$screen_id, 'side', null );
239 echo ' </div>';
240 echo ' <div id="postbox-container-3" class="postbox-container">';
241 do_meta_boxes( self::$screen_id, 'column3', null );
242 echo ' </div>';
243 echo ' <div id="postbox-container-4" class="postbox-container">';
244 do_meta_boxes( self::$screen_id, 'column4', null );
245 echo ' </div>';
246 echo ' </div>';
247 echo '</div>';
248 echo '</form>';
249 } else {
250 echo '<h2>' . decalog_esc_html__( 'Forbidden', 'decalog' ) . '</h2>';
251 settings_errors();
252 echo '<p>' . decalog_esc_html__( 'The trace or traces log you tried to access is out of your scope.', 'decalog' ) . '</p>';
253 echo '<p>' . decalog_esc_html__( 'If you think this is an error, please contact the network administrator with these details:', 'decalog' );
254 echo '<ul>';
255 // phpcs:ignore
256 echo '<li>' . sprintf( decalog_esc_html__( 'Traces log: %s', 'decalog' ), '<code>' . $this->logid . '</code>' ) . '</li>';
257 // phpcs:ignore
258 echo '<li>' . sprintf( decalog_esc_html__( 'Trace: %s', 'decalog' ), '<code>' . $this->traceid . '</code>' ) . '</li>';
259 echo '</ul>';
260 echo '</p>';
261 $this->logger->warning( sprintf( 'Trying to access out of scope trace #%s from traces log {%s}.', $this->traceid, $this->logid ), 403 );
262 }
263 echo '</div>';
264 }
265
266 /**
267 * Print the single span visualization.
268 *
269 * @since 3.0.0
270 */
271 public function get_span( $span, $level = 0, $start = 0, $duration = 0 ) {
272 $result = '<div class="decalog-span-wrap">';
273 // Span text
274 $result .= '<div class="decalog-span-text">';
275 $result .= str_pad( '', ( $level * 3 ) * 6, '&nbsp;' ) . '<strong>' . $span['resource'] . '</strong>' . ( 0 === $level ? '' : '&nbsp;&nbsp;' . $span['name'] );
276 $result .= '</div>';
277 // Span timeline
278 $bblank = round( 100 * ( $span['start'] - $start ) / $duration, 2 );
279 $lblank = round( 100 * ( $span['duration'] ) / $duration, 2 );
280 if ( 0.1 > $lblank ) {
281 $lblank = 0.1;
282 }
283 $eblank = 100.0 - $bblank - $lblank;
284 $tick = round( 100 * 500000 / $duration, 2 );
285 $color = $this->colors['wordpress'];
286 if ( array_key_exists( strtolower( $span['resource'] ), $this->colors ) ) {
287 $color = $this->colors[ strtolower( $span['resource'] ) ];
288 }
289 $style = '';
290 if ( 0 > $eblank ) {
291 $eblank = 0.0;
292 $bblank = 100.0 - $lblank;
293 }
294 if ( 90 > $lblank ) {
295 if ( 10 < $eblank ) {
296 $style = 'style="margin-left:100%;"';
297 }
298 }
299 if ( 90 > $bblank ) {
300 $result .= '<div class="decalog-span-timeline" style="background-size: ' . $tick . '% 100%;">';
301 $result .= '<div class="decalog-span-timeline-blank" style="width:' . $bblank . '%">';
302 $result .= '</div>';
303 $result .= '<div class="decalog-span-timeline-line" style="background-color:' . $color . ';width:' . $lblank . '%">';
304 $result .= '<span class="decalog-span-timeline-text" ' . $style . '>' . (int) round( ( $span['duration'] / 1000 ), 0 ) . '&nbsp;ms</span>';
305 $result .= '</div>';
306 $result .= '<div class="decalog-span-timeline-blank" style="width:' . $eblank . '%;">';
307 $result .= '</div>';
308 $result .= '</div>';
309 $result .= '</div>';
310 } else {
311 $result .= '<div class="decalog-span-timeline" style="background-size: ' . $tick . '% 100%;">';
312 $result .= '<div class="decalog-span-timeline-blank" style="width:' . $bblank . '%;vertical-align: middle;">';
313 $result .= '<span class="decalog-span-timeline-text" style="float:right">' . (int) round( ( $span['duration'] / 1000 ), 0 ) . '&nbsp;ms</span>';
314 $result .= '</div>';
315 $result .= '<div class="decalog-span-timeline-line" style="background-color:' . $color . ';width:' . $lblank . '%">';
316 $result .= '</div>';
317 $result .= '<div class="decalog-span-timeline-blank" style="width:' . $eblank . '%;">';
318 $result .= '</div>';
319 $result .= '</div>';
320 $result .= '</div>';
321 }
322
323
324
325
326 if ( $span['subspans'] && is_array( $span['subspans'] ) && 0 < count( $span['subspans'] ) ) {
327 foreach ( $span['subspans'] as $s ) {
328 $result .= $this->get_span( $s, $level + 1, $start, $duration );
329 }
330 }
331 return $result;
332 }
333
334 /**
335 * Print the trace visualization.
336 *
337 * @since 3.0.0
338 */
339 public function get_spans() {
340 $result = '<div class="decalog-spans-wrap" style="width:100%">';
341 if ( $this->trace['spans'] && is_array( $this->trace['spans'] ) && 0 < count( $this->trace['spans'] ) ) {
342 foreach ( $this->trace['spans'] as $span ) {
343 $result .= $this->get_span( $span, 0, $span['start'], $span['duration'] );
344 }
345 } else {
346 $result .= 'NOTHING';
347 }
348 $result .= '</div>';
349 $this->output_activity_block( $result, 'none' );
350 }
351
352 /**
353 * Add footer scripts.
354 *
355 * @since 3.0.0
356 */
357 public function add_footer() {
358 $result = '<script>';
359 $result .= ' jQuery(document).ready( function($) {';
360 $result .= " $('.if-js-closed').removeClass('if-js-closed').addClass('closed');";
361 $result .= " if(typeof postboxes !== 'undefined')";
362 $result .= " postboxes.add_postbox_toggles('" . self::$screen_id . "');";
363 $result .= ' });';
364 $result .= '</script>';
365 // phpcs:ignore
366 echo $result;
367 }
368
369 /**
370 * Add all the needed meta boxes.
371 *
372 * @since 3.0.0
373 */
374 public function add_metaboxes() {
375 // Full column.
376 add_meta_box( 'decalog-tspans', decalog_esc_html__( 'Spans', 'decalog' ), [ $this, 'get_spans' ], self::$screen_id, 'full' );
377 // Left column.
378 add_meta_box( 'decalog-tmain', decalog_esc_html__( 'Trace', 'decalog' ), [ $this, 'trace_widget' ], self::$screen_id, 'advanced' );
379 // Right column.
380 add_meta_box( 'decalog-twordpress', 'WordPress', [ $this, 'wordpress_widget' ], self::$screen_id, 'side' );
381 }
382
383 /**
384 * Get the action image.
385 *
386 * @param string $url The url to call.
387 * @param string $text The anchor to display.
388 * @return string The action image, ready to print.
389 * @since 3.3.0
390 */
391 protected function get_action( $url, $text ) {
392 return '<a href="' . $url . '" target="_blank">' . esc_html( $text ) . '</a>';
393 }
394
395 /**
396 * Get the action image.
397 *
398 * @param string $box The box where to display actions.
399 * @return string The actions texts, ready to print.
400 * @since 3.3.0
401 */
402 protected function get_actions( $box ) {
403 $item = $this->trace;
404 $item['logger_id'] = $this->logid;
405
406 /**
407 * Filters the available actions for the current item.
408 *
409 * @See https://github.com/Pierre-Lannoy/wp-decalog/blob/master/HOOKS.md
410 * @since 3.3.0
411 * @param array $item The full trace with metadata.
412 */
413 $actions = apply_filters( 'decalog_trace_view_actions_for_' . $box, [], $item );
414
415 $result = '';
416 $list = [];
417 foreach ( $actions as $action ) {
418 if ( isset( $action['url'] ) && isset( $action['text'] ) ) {
419 $list[] = $this->get_action( $action['url'], $action['text'] );
420 }
421 }
422 if ( 0 < count( $list ) ) {
423 $result = implode( ' | ', $list );
424 }
425 return $result;
426 }
427
428 /**
429 * Print an activity block.
430 *
431 * @param string $content The content of the block.
432 * @param string $box The corresponding box id.
433 * @since 1.0.0
434 */
435 private function output_activity_block( $content, $box ) {
436 $actions = $this->get_actions( $box );
437 echo '<div class="activity-block" style="padding-bottom: 0;padding-top: 0;">';
438 // phpcs:ignore
439 echo $content;
440 if ( '' !== $actions ) {
441 echo '<div style="font-size:x-small; margin-bottom:-4px;text-align:right">' . $actions . '</div>';
442 }
443 echo '</div>';
444 }
445
446 /**
447 * Get a section to include in a block.
448 *
449 * @param string $content The content of the section.
450 * @return string The section, ready to print.
451 * @since 3.0.0
452 */
453 private function get_section( $content ) {
454 return '<div style="margin-bottom: 10px;">' . $content . '</div>';
455 }
456
457 /**
458 * Get an icon.
459 *
460 * @param string $icon_name The name of the icon.
461 * @param string $background The background color.
462 * @return string The icon, as image, ready to print.
463 * @since 3.0.0
464 */
465 private function get_icon( $icon_name, $background = '#F9F9F9' ) {
466 return '<img style="width:18px;float:left;padding-right:6px;" src="' . Feather\Icons::get_base64( $icon_name, $background, '#9999BB' ) . '" />';
467 }
468
469 /**
470 * Get an external link markup.
471 *
472 * @param string $url The url.
473 * @return string The link markup, ready to print.
474 * @since 3.0.0
475 */
476 private function get_external_link( $url ) {
477 if ( '' === $url ) {
478 return '';
479 }
480 return '<a href="' . $url . '" target="_blank"><img style="width:10px;padding-left:4px;padding-right:6px;vertical-align:text-top;" src="' . Feather\Icons::get_base64( 'external-link', 'none', '#9999BB' ) . '" /></a>';
481 }
482
483 /**
484 * Get an intenel link markup.
485 *
486 * @param string $url The url.
487 * @param string $anchor The anchor.
488 * @return string The link markup, ready to print.
489 * @since 3.0.0
490 */
491 private function get_internal_link( $url, $anchor ) {
492 if ( '' === $url ) {
493 return $anchor;
494 }
495 return '<a href="' . $url . '" style="text-decoration:none;color:inherit;">' . $anchor . '</a>';
496 }
497
498 /**
499 * Get content of the event widget box.
500 *
501 * @since 3.0.0
502 */
503 public function trace_widget() {
504 // Trace type.
505 $icon = '<img style="width:18px;float:left;padding-right:6px;" src="' . Icons::get_base64( 'clock', '#ABCFF9', '#192783' ) . '" />';
506 $level = decalog_esc_html__( 'Trace', 'decalog' );
507 $channel = ChannelTypes::$channel_names[ strtoupper( $this->trace['channel'] ) ];
508 $content = '<span style="width:40%;cursor: default;float:left">' . $icon . $level . '</span>';
509 $content .= '<span style="width:60%;cursor: default;">' . $this->get_icon( 'activity', 'none' ) . $channel . '</span>';
510 $trace = $this->get_section( $content );
511 // Trace time.
512 $time = Date::get_date_from_mysql_utc( $this->trace['timestamp'], Timezone::network_get()->getName(), 'Y-m-d H:i:s' );
513 $dif = Date::get_positive_time_diff_from_mysql_utc( $this->trace['timestamp'] );
514 $content = '<span style="width:100%;cursor: default;">' . $this->get_icon( 'clock' ) . $time . '</span> <span style="color:silver">(' . $dif . ')</span>';
515 $hour = $this->get_section( $content );
516 // Instance.
517 $content = '<span style="width:100%;cursor: default;">' . $this->get_icon( 'cpu' ) . $this->trace['instance'] . '</span>';
518 $instance = $this->get_section( $content );
519
520 $this->output_activity_block( $trace . $hour . $instance, 'trace' );
521 }
522
523 /**
524 * Get content of the WordPress widget box.
525 *
526 * @since 3.0.0
527 */
528 public function wordpress_widget() {
529 // User detail.
530 if ( 'anonymous' === $this->trace['user_name'] ) {
531 $user = $this->get_section( '<span style="width:100%;cursor: default;word-break: break-all;">' . $this->get_icon( 'user' ) . decalog_esc_html__( 'Anonymous user', 'decalog' ) . '</span>' );
532 } elseif ( 0 === strpos( $this->trace['user_name'], '{' ) ) {
533 $user = $this->get_section( '<span style="width:100%;cursor: default;word-break: break-all;">' . $this->get_icon( 'user' ) . decalog_esc_html__( 'Pseudonymized user', 'decalog' ) . '</span>' );
534 } elseif ( 0 !== (int) $this->trace['user_id'] ) {
535 $user = $this->get_section( '<span style="width:100%;cursor: default;word-break: break-all;">' . $this->get_icon( 'user-check' ) . User::get_user_string( (int) $this->trace['user_id'] ) . '</span>' );
536 } else {
537 $user = $this->get_section( '<span style="width:100%;cursor: default;word-break: break-all;">' . $this->get_icon( 'user-x' ) . decalog_esc_html__( 'Deleted user', 'decalog' ) . '</span>' );
538 }
539 // Site detail.
540 $content = '<span style="width:100%;cursor: default;">' . $this->get_icon( 'layout' ) . $this->trace['site_name'] . '</span>';
541 $site = $this->get_section( $content );
542 $this->output_activity_block( $user . $site, 'wp' );
543 }
544
545 }
546