Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
0.00% covered (danger)
0.00%
0 / 53
0.00% covered (danger)
0.00%
0 / 5
CRAP
0.00% covered (danger)
0.00%
0 / 1
ParsoidLogger
0.00% covered (danger)
0.00%
0 / 53
0.00% covered (danger)
0.00%
0 / 5
702
0.00% covered (danger)
0.00%
0 / 1
 buildLoggingRE
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 __construct
0.00% covered (danger)
0.00%
0 / 10
0.00% covered (danger)
0.00%
0 / 1
30
 formatTrace
0.00% covered (danger)
0.00%
0 / 18
0.00% covered (danger)
0.00%
0 / 1
20
 logMessage
0.00% covered (danger)
0.00%
0 / 10
0.00% covered (danger)
0.00%
0 / 1
42
 log
0.00% covered (danger)
0.00%
0 / 14
0.00% covered (danger)
0.00%
0 / 1
110
1<?php
2declare( strict_types = 1 );
3
4namespace Wikimedia\Parsoid\Logger;
5
6use Psr\Log\LoggerInterface;
7use Psr\Log\LogLevel;
8use Wikimedia\Parsoid\Utils\PHPUtils;
9
10class ParsoidLogger {
11    /* @var Logger */
12    private $backendLogger;
13
14    /* @var string|null Null means nothing is enabled */
15    private $enabledRE = null;
16
17    /** PORT-FIXME: Not yet implemented. Monolog supports sampling as well! */
18    /* @var string */
19    private $samplingRE;
20
21    private const PRETTY_LOGTYPE_MAP = [
22        'debug' => '[DEBUG]',
23        'trace/peg' => '[peg]',
24        'trace/pre' => '[PRE]',
25        'debug/pre' => '[PRE-DBG]',
26        'trace/p-wrap' => '[P]',
27        'trace/html' => '[HTML]',
28        'debug/html' => '[HTML-DBG]',
29        'trace/sanitizer' => '[SANITY]',
30        'trace/tsp' => '[TSP]',
31        'trace/dsr' => '[DSR]',
32        'trace/list' => '[LIST]',
33        'trace/quote' => '[QUOTE]',
34        'trace/wts' => '[WTS]',
35        'debug/wts' => '---[WTS-DBG]---',
36        'debug/wts/sep' => '[SEP]',
37        'trace/selser' => '[SELSER]',
38        'trace/domdiff' => '[DOM-DIFF]',
39        'trace/wt-escape' => '[wt-esc]',
40        'trace/ttm:2' => '[2-TTM]',
41        'trace/ttm:3' => '[3-TTM]',
42    ];
43
44    /**
45     * TRACE / DEBUG: Make trace / debug regexp with appropriate postfixes,
46     * depending on the command-line options passed in.
47     *
48     * @param array $flags
49     * @param string $logType
50     * @return string
51     */
52    private function buildLoggingRE( array $flags, string $logType ): string {
53        return $logType . '/(?:' . implode( '|', array_keys( $flags ) ) . ')(?:/|$)';
54    }
55
56    /**
57     * @param LoggerInterface $backendLogger
58     * @param array $options
59     * - logLevels  string[]
60     * - traceFlags <string,bool>[]
61     * - debugFlags <string,bool>[]
62     * - dumpFlags  <string,bool>[]
63     */
64    public function __construct( LoggerInterface $backendLogger, array $options ) {
65        $this->backendLogger = $backendLogger;
66
67        $rePatterns = $options['logLevels'];
68        if ( $options['traceFlags'] ) {
69            $rePatterns[] = $this->buildLoggingRE( $options['traceFlags'], 'trace' );
70        }
71        if ( $options['debugFlags'] ) {
72            $rePatterns[] = $this->buildLoggingRE( $options['debugFlags'], 'debug' );
73        }
74        if ( $options['dumpFlags'] ) {
75            // For tracing, Parsoid simply calls $env->log( "trace/SOMETHING", ... );
76            // The filtering based on whether a trace is enabled is handled by this class.
77            // This is done via the regexp pattern being constructed above.
78            // However, for dumping, at some point before / during the port from JS to PHP,
79            // all filtering is being done at the site of constructing dumps. This might
80            // have been because of the expensive nature of dumps, but closures could have
81            // been used. In any case, given that usage, we don't need to do any filtering
82            // here. The only caller for dumps is Env.php::writeDump right now which is
83            // called after filtering for enabled flags, and which calls us with a "dump"
84            // prefix. So, all we need to do here is enable the 'dump' prefix without
85            // processing CLI flags. In the future, if those dump logging call sites go
86            // back to usage like $env->log( "dump/dom:post-dsr", ... ), etc. we can
87            // switch this back to constructing a regexp.
88            $rePatterns[] = 'dump'; // $this->buildLoggingRE( $options['dumpFlags'], 'dump' );
89        }
90
91        if ( count( $rePatterns ) > 0 ) {
92            $this->enabledRE = '#^(?:' . implode( '|', $rePatterns ) . ')#';
93        }
94    }
95
96    /**
97     * PORT-FIXME: This can become a MonologFormatter possibly.
98     *
99     * We can create channel-specific loggers and this formatter
100     * can be added to the trace-channel logger.
101     *
102     * @param string $logType
103     * @param array $args
104     * @return string
105     */
106    private function formatTrace( string $logType, array $args ): string {
107        $typeColumnWidth = 15;
108        $firstArg = $args[0];
109
110        // Assume first numeric arg is always the pipeline id
111        if ( is_numeric( $firstArg ) ) {
112            $msg = $firstArg . '-';
113            array_shift( $args );
114        } else {
115            $msg = '';
116        }
117
118        // indent by number of slashes
119        $numMatches = substr_count( $logType, '/' );
120        $indent = str_repeat( '  ', $numMatches > 1 ? $numMatches - 1 : 0 );
121        $msg .= $indent;
122
123        $prettyLogType = self::PRETTY_LOGTYPE_MAP[$logType] ?? null;
124        if ( $prettyLogType ) {
125            $msg .= $prettyLogType;
126        } else {
127            // XXX: could shorten or strip trace/ logType prefix in a pure trace logger
128            $msg .= $logType;
129
130            // More space for these log types
131            $typeColumnWidth = 30;
132        }
133
134        // Fixed-width type column so that the messages align
135        $msg = substr( $msg, 0, $typeColumnWidth );
136        $msg .= str_repeat( ' ', $typeColumnWidth - strlen( $msg ) );
137        $msg .= '|' . $indent . $this->logMessage( null, $args );
138
139        return $msg;
140    }
141
142    /**
143     * @param ?string $logType
144     * @param array $args
145     * @return string
146     */
147    private function logMessage( ?string $logType, array $args ): string {
148        $numArgs = count( $args );
149        $output = $logType ? "[$logType]" : '';
150        foreach ( $args as $arg ) {
151            // don't use is_callable, it would return true for any string that happens to be a function name
152            if ( $arg instanceof \Closure ) {
153                $output .= ' ' . $arg();
154            } elseif ( is_string( $arg ) ) {
155                if ( strlen( $arg ) ) {
156                    $output .= ' ' . $arg;
157                }
158            } else {
159                $output .= PHPUtils::jsonEncode( $arg );
160            }
161        }
162
163        return $output;
164    }
165
166    /**
167     * @param string $prefix
168     * @param mixed ...$args
169     */
170    public function log( string $prefix, ...$args ): void {
171        // FIXME: This requires enabled loglevels to percolate all the way here!!
172        // Quick check for un-enabled logging.
173        if ( !$this->enabledRE || !preg_match( $this->enabledRE, $prefix ) ) {
174            return;
175        }
176
177        // PORT-FIXME: Are there any instances where we will have this?
178        if ( $this->backendLogger instanceof \Psr\Log\NullLogger ) {
179            // No need to build the string if it's going to be thrown away anyway.
180            return;
181        }
182
183        $logLevel = strstr( $prefix, '/', true ) ?: $prefix;
184
185        // Handle trace type first
186        if ( $logLevel === 'trace' || $logLevel === 'debug' ) {
187            $this->backendLogger->log( LogLevel::DEBUG, $this->formatTrace( $prefix, $args ) );
188        } else {
189            if ( $logLevel === 'dump' ) {
190                $logLevel = LogLevel::DEBUG;
191            } elseif ( $logLevel === 'fatal' ) {
192                $logLevel = LogLevel::CRITICAL;
193            } elseif ( $logLevel === 'warn' ) {
194                $logLevel = LogLevel::WARNING;
195            }
196            $this->backendLogger->log( $logLevel, $this->logMessage( $prefix, $args ) );
197        }
198    }
199}