1 <?php
2 /**
3 * Contains Log class for Alphred, providing basic logging functionality
4 *
5 * PHP version 5
6 *
7 * @package Alphred
8 * @copyright Shawn Patrick Rice 2014
9 * @license http://opensource.org/licenses/MIT MIT
10 * @version 1.0.0
11 * @author Shawn Patrick Rice <rice@shawnrice.org>
12 * @link http://www.github.com/shawnrice/alphred
13 * @link http://shawnrice.github.io/alphred
14 * @since File available since Release 1.0.0
15 *
16 */
17
18 namespace Alphred;
19
20 /**
21 * Simple static logging functionality that writes to files or STDERR
22 *
23 * @package Alphred
24 * @since Class available since 1.0.0
25 *
26 */
27 class Log {
28
29 /**
30 * An array that contans the valid log levels
31 * @since 1.0.0
32 * @var array
33 */
34 static $log_levels = [
35 0 => 'DEBUG',
36 1 => 'INFO',
37 2 => 'WARNING',
38 3 => 'ERROR',
39 4 => 'CRITICAL',
40 ];
41
42 /**
43 * Log a message to a file
44 *
45 * @since 1.0.0
46 *
47 * @param string $message the message to log
48 * @param integer|string $level the log level
49 * @param string $filename the filename of the log without an extension
50 * @param boolean|integer $trace how far back to trace
51 */
52 public function file( $message, $level = 1, $filename = 'workflow', $trace = false ) {
53
54 // Check if the log level is loggable based on the threshold.
55 // The threshold is defined as the constant ALPHED_LOG_LEVEL, and defaults to level 2 (WARNING).
56 // Change this either in the workflow.ini file or by defining the constant ALPHRED_LOG_LEVEL
57 // before you include Alphred.phar.
58 if ( ! self::is_loggable( $level ) ) {
59 return false;
60 }
61
62 // Get the full path to the log file, and create the data directory if it doesn't exist
63 $log_file = self::get_log_filename( $filename );
64 // Get the trace
65 $trace = self::trace( $trace );
66 // Get the formatted date
67 $date = self::date_file();
68 // Normalize the log level
69 $level = self::normalize_log_level( $level );
70
71 // Construct the log entry
72 $message = "[{$date}][{$trace}][{$level}] {$message}\n";
73
74 // Write to the log file
75 file_put_contents( $log_file, $message, FILE_APPEND | LOCK_EX );
76 }
77
78 /**
79 * Log a message to the console (STDERR)
80 *
81 * @since 1.0.0
82 *
83 * @param string $message the message to log
84 * @param string|integer $level the log level
85 * @param boolean|integer $trace how far back to trace
86 */
87 public function console( $message, $level = 1, $trace = false ) {
88
89 // Check if the log level is loggable based on the threshold.
90 // The threshold is defined as the constant ALPHED_LOG_LEVEL, and defaults to level 2 (WARNING).
91 // Change this either in the workflow.ini file or by defining the constant ALPHRED_LOG_LEVEL
92 // before you include Alphred.phar.
93 if ( ! self::is_loggable( $level ) ) {
94 return false;
95 }
96
97 // Get the trace
98 $trace = self::trace( $trace );
99 // Get the formatted date
100 $date = self::date_console();
101 // Normalize the log level
102 $level = self::normalize_log_level( $level );
103 file_put_contents( 'php://stderr', "[{$date}][{$trace}][{$level}] {$message}\n" );
104 }
105
106 /**
107 * Log a message to both a file and the console
108 *
109 * @since 1.0.0
110 *
111 * @param string $message the message to log
112 * @param integer|string $level the log level
113 * @param string $filename the filename of the log without an extension
114 * @param boolean|integer $trace how far back to trace
115 */
116 public function log( $message, $level = 1, $filename = 'workflow', $trace = false ) {
117 // Check if the log level is loggable based on the threshold.
118 // The threshold is defined as the constant ALPHED_LOG_LEVEL, and defaults to level 2 (WARNING).
119 // Change this either in the workflow.ini file or by defining the constant ALPHRED_LOG_LEVEL
120 // before you include Alphred.phar.
121 if ( ! self::is_loggable( $level ) ) {
122 return false;
123 }
124 // Log message to console
125 self::console( $message, $level, $trace );
126 // Log message to file
127 self::file( $message, $level, $filename, $trace );
128 }
129
130 /**
131 * Gets the full filepath to the log file
132 *
133 * @since 1.0.0
134 *
135 * @param string $filename a filename for a log file
136 * @return string the full filepath for a log file
137 */
138 private function get_log_filename( $filename ) {
139 // Attempt to get the workflow's data directory. If it isn't set (i.e. running outside of a workflow env),
140 // then just use the current directory.
141 if ( ! $dir = \Alphred\Globals::get( 'alfred_workflow_data' ) ) {
142 $dir = '.';
143 } else {
144 self::create_log_directory();
145 }
146 return "{$dir}/{$filename}.log";
147 }
148
149 /**
150 * Creates the workflow's data directory if it does not exist.
151 *
152 * @since 1.0.0
153 */
154 private function create_log_directory() {
155 $directory = \Alphred\Globals::get( 'alfred_workflow_data' );
156 if ( $directory ) {
157 if ( ! file_exists( $directory ) ) {
158 mkdir( $directory, 0775, true );
159 }
160 }
161 }
162
163 /**
164 * Checks to see if the log needs to be rotated
165 *
166 * @since 1.0.0
167 */
168 private function check_log( $filename ) {
169 // ALPHRED_LOG_SIZE is define in bytes. It defaults to 1048576 and is set in
170 // `Alphred.php`. If you want to change the max size, then either define the
171 // max size in the INI file or define the constant ALPHRED_LOG_SIZE before
172 // you include `Alphred.phar`.
173 if ( filesize( self::get_log_filename( $filename ) ) > ALPHRED_LOG_SIZE ) {
174 // The log is too big, so rotate it.
175 self::rotate_log( $filename );
176 }
177 }
178
179
180 /**
181 * Rotates the log
182 *
183 * @since 1.0.0
184 */
185 private function rotate_log( $filename ) {
186 // Get the log filename
187 $log_file = self::get_log_filename( $filename );
188
189 // Set the backup log filename
190 $old = substr( $log_file, -4 ) . '.1.log';
191
192 // Check if an old filelog exists
193 if ( file_exists( $old ) ) {
194 // It exists, so delete it
195 unlink( $old );
196 }
197
198 // Rename the current log to the old log
199 rename( $log_file, $old );
200
201 // Create an empty log file
202 file_put_contents( $log_file, '' );
203 }
204
205 /**
206 * Normalizes the log level, returning 'INFO' or 1 if invalid
207 *
208 * @since 1.0.0
209 *
210 * @param integer|string $level the level represented as either a string or an integer
211 * @return string the name of the log level
212 */
213 private function normalize_log_level( $level ) {
214 // Check if the log level is numeric
215 if ( is_numeric( $level ) ) {
216 // It is numeric, so check if it is valid
217 if ( isset( self::$log_levels[ $level ] ) ) {
218 // It is valid, so return the name of the level
219 return self::$log_levels[ $level ];
220 } else {
221 // The level is numeric but not valid, so log a warning to the console, and
222 // return log level 1.
223 self::console( "Log level {$level} is not valid. Setting to log level 1." );
224 return self::$log_levels[1]; // This is an assumption note an error here
225 }
226 }
227 // It is not numeric, so check if it is in the log levels array
228 if ( in_array( $level, self::$log_levels ) ) {
229 // It is in the array, so return the value passed
230 return $level;
231 }
232 // The log level is a string but is not valid, so log an error to the console
233 // and return log level 1.
234 self::console( "Log level {$level} is not valid. Setting to log level 1." );
235 return self::$log_levels[1]; // This is an assumption, note an error here
236 }
237
238 /**
239 * Fetches information from a stack trace
240 *
241 * @since 1.0.0
242 *
243 * @param boolean|integer $depth How far to do the trace, default is the last
244 * @return string the file and line number of the trace
245 */
246 private function trace( $depth = false ) {
247
248 // Get the relevant information from the backtrace
249 $trace = debug_backtrace();
250 // Check if the dpeth is defined, and if the depth is within the trace
251 if ( $depth && isset( $trace[ $depth ] ) ) {
252 // The depth is defined, see if it is negative
253 if ( $depth < 0 ) {
254 // It's negative, so translate that to a positive number that we can use.
255 $depth = count( $trace ) + $depth - 1;
256 }
257 // Get the explicit trace
258 $trace = $trace[ $depth ];
259 } else {
260 // Just get the last trace.
261 $trace = end( $trace );
262 }
263
264 // Set the filename
265 $file = basename( $trace['file'] );
266 // Set the line number
267 $line = $trace['line'];
268
269 return "{$file}:{$line}";
270 }
271
272
273 /**
274 * Checks if a log level is within a display threshold
275 *
276 * @since 1.0.0
277 *
278 * @param mixed $level Either a string or a
279 * @return boolean Whether or not a value is above the logging threshold
280 */
281 private function is_loggable( $level ) {
282 // First, check is the level is numeric
283 if ( ! is_numeric( $level ) ) {
284 // It is not numeric, so let's translate it to a number
285 $level = array_search( $level, self::$log_levels ); // This needs error checking
286 }
287 // Return a boolean of whether or not the level is less than or equal to the logging threshold
288 return $level >= self::get_threshold();
289 }
290
291 /**
292 * Gets the threshold for log messages
293 *
294 * @todo Implement exception for bad log level
295 * @since 1.0.0
296 *
297 * @return integer an integer matching a log level
298 */
299 private function get_threshold() {
300 // Check is the threshold is defined as a number
301 if ( is_numeric( ALPHRED_LOG_LEVEL ) ) {
302 // It is, so just return that number
303 return ALPHRED_LOG_LEVEL;
304 } else if ( in_array( ALPHRED_LOG_LEVEL, self::$log_levels ) ) {
305 // The threshold is not defined as a number, but it is a string defined
306 // in the log_levels, so return the number
307 return array_search( ALPHRED_LOG_LEVEL, self::$log_levels );
308 } else {
309 // The threshold is not a number, and it is not a string that is in the
310 // log_levels, so throw an exception and return 0.
311 throw new Exception( "Alphred Log Level is not a valid level" );
312 return 0;
313 }
314 }
315
316 /**
317 * Gets the time formatted for a console display log
318 *
319 * @since 1.0.0
320 *
321 * @return string the time as HH:MM:SS
322 */
323 private function date_console() {
324 return date( 'H:i:s', time() );
325 }
326
327 /**
328 * Gets a datestamp formatted for a file log
329 *
330 * @since 1.0.0
331 *
332 * @return string Formatted as YYYY-MM-DD HH:MM:SS
333 */
334 private function date_file() {
335 return date( 'Y-m-d H:i:s' );
336 }
337 }