1: <?php
2:
3: /**
4: * Alfred Bundler Logging Fiel
5: *
6: * Generic interface for logging to files and the console to be used with the
7: * PHP implementation of the Alfred Bundler.
8: *
9: * This file is part of the Alfred Bundler, released under the MIT licence.
10: * Copyright (c) 2014 The Alfred Bundler Team
11: * See https://github.com/shawnrice/alfred-bundler for more information
12: *
13: * @copyright The Alfred Bundler Team 2014
14: * @license http://opensource.org/licenses/MIT MIT
15: * @version Taurus 1
16: * @link http://shawnrice.github.io/alfred-bundler
17: * @package AlfredBundler
18: * @since File available since Taurus 1
19: */
20:
21:
22: if ( ! class_exists( 'AlfredBundlerLogger' ) ) :
23: /**
24: *
25: * Simple logging functionality that writes to files or STDERR
26: *
27: * Usage: just create a single object and reuse it solely. Initialize the
28: * object with a full path to the log file (no log extension)
29: *
30: * This class was written to be part of the PHP implementation of the
31: * Alfred Bundler for the bundler's internal logging requirements. However
32: * its functionality is also made available to any workflow author implementing
33: * the bundler.
34: *
35: * While you can use this class without going through the bundler, it is
36: * easier just to use the logging functionality indirectly via the
37: * bundler object.
38: *
39: * @see AlfredBundlerInternalClass::log
40: * @see AlfredBundlerInternalClass::debug
41: * @see AlfredBundlerInternalClass::info
42: * @see AlfredBundlerInternalClass::warning
43: * @see AlfredBundlerInternalClass::error
44: * @see AlfredBundlerInternalClass::critical
45: * @see AlfredBundlerInternalClass::console
46: *
47: * @package AlfredBundler
48: * @since Class available since Taurus 1
49: *
50: */
51: class AlfredBundlerLogger {
52:
53: /**
54: * Log file
55: *
56: * Full path to log file with no extension; set by user at instantiation
57: *
58: * @var string
59: * @since Taurus 1
60: */
61: public $log;
62:
63: /**
64: * An array of log levels (int => string )
65: *
66: * 0 => 'DEBUG'
67: * 1 => 'INFO'
68: * 2 => 'WARNING'
69: * 3 => 'ERROR'
70: * 4 => 'CRITICAL'
71: *
72: * @var array
73: * @since Taurus 1
74: */
75: protected $logLevels;
76:
77: /**
78: * Stacktrace information; reset with each message
79: *
80: * @var array
81: * @since Taurus 1
82: */
83: private $trace;
84:
85: /**
86: * File from stacktrace; reset with each message
87: *
88: * @var string
89: * @since Taurus 1
90: */
91: private $file;
92:
93: /**
94: * Line from stacktrace; reset with each message
95: *
96: * @var int
97: * @since Taurus 1
98: */
99: private $line;
100:
101: /**
102: * Log level; reset with each message
103: *
104: * @var mixed
105: * @since Taurus 1
106: */
107: private $level;
108:
109: /**
110: * Default destination to send a log message to
111: *
112: * @var string options: file, console, both
113: */
114: private $defaultDestination;
115:
116: /**
117: * Sets variables and ini settings to ensure there are no errors
118: *
119: * @param string $log filename to use as a log
120: * @param string $destination = 'file' default destination for messages
121: * @since Taurus 1
122: */
123: public function __construct( $log, $destination = 'file' ) {
124:
125: $this->log = $log . '.log';
126: $this->initializeLog();
127:
128: if ( ! in_array( $destination, [ 'file', 'console', 'both' ] ) )
129: $this->defaultDestination = 'file';
130: else
131: $this->defaultDestination = $destination;
132:
133: // These are the appropriate log levels
134: $this->logLevels = array( 0 => 'DEBUG',
135: 1 => 'INFO',
136: 2 => 'WARNING',
137: 3 => 'ERROR',
138: 4 => 'CRITICAL',
139: );
140:
141: // Set date/time to avoid warnings/errors.
142: if ( ! ini_get( 'date.timezone' ) ) {
143: $tz = exec( 'tz=`ls -l /etc/localtime` && echo ${tz#*/zoneinfo/}' );
144: ini_set( 'date.timezone', $tz );
145: }
146:
147: // This is needed because, Macs don't read EOLs well.
148: if ( ! ini_get( 'auto_detect_line_endings' ) )
149: ini_set( 'auto_detect_line_endings', TRUE );
150:
151: }
152:
153: /**
154: * Logs a message to either a file or STDERR
155: *
156: * After initializing the log object, this should be the only
157: * method with which you interact.
158: *
159: * While you could use this separate from the bundler itself, it is
160: * easier to use the logging functionality from the bundler object.
161: *
162: * @see AlfredBundlerInternalClass::log
163: *
164: * <code>
165: * $log = new AlfredBundlerLogger( '/full/path/to/mylog' );
166: * $log->log( 'Write this to a file', 'INFO' );
167: * $log->log( 'Warning message to console', 2, 'console' );
168: * $log->log( 'This message will go to both the console and the log', 3, 'both');
169: * </code>
170: *
171: *
172: * @param string $message message to log
173: * @param mixed $level either int or string of log level
174: * @param string $destination where the message should appear:
175: * valid options: 'file', 'console', 'both'
176: * @since Taurus 1
177: */
178: public function log( $message, $level = 'INFO', $destination = '', $trace = 0 ) {
179:
180: // Set the destination to the default if not implied
181: if ( empty( $destination ) )
182: $destination = $this->defaultDestination;
183:
184: // print_r( debug_backtrace() );
185:
186: // Get the relevant information from the backtrace
187: $this->trace = debug_backtrace();
188: $this->trace = $this->trace[ $trace ];
189: $this->file = basename( $this->trace[ 'file' ] );
190: $this->line = $this->trace[ 'line' ];
191:
192: // check / normalize the arguments
193: $this->level = $this->normalizeLogLevel( $level );
194: $destination = strtolower( $destination );
195:
196: if ( $destination == 'file' || $destination == 'both' )
197: $this->logFile( $message );
198: if ( $destination == 'console' || $destination == 'both' )
199: $this->logConsole( $message );
200:
201: }
202:
203: /**
204: * Creates log directory and file if necessary
205: * @since Taurus 1
206: */
207: private function initializeLog() {
208: if ( ! file_exists( $this->log ) ) {
209: if ( ! is_dir( realpath( dirname( $this->log ) ) ) )
210: mkdir( dirname( $this->log ), 0775, TRUE );
211: file_put_contents( $this->log, '' );
212: }
213: }
214:
215:
216: /**
217: * Checks to see if the log needs to be rotated
218: * @since Taurus 1
219: */
220: private function checkLog() {
221: if ( filesize( $this->log ) > 1048576 )
222: $this->rotateLog();
223: }
224:
225:
226: /**
227: * Rotates the log
228: * @since Taurus 1
229: */
230: private function rotateLog() {
231: $old = substr( $this->log, -4 );
232: if ( file_exists( $old . '1.log' ) )
233: unlink( $old . '1.log' );
234:
235: rename( $this->log, $old . '1.log' );
236: file_put_contents( $this->log, '' );
237: }
238:
239: /**
240: * Ensures that the log level is valid
241: *
242: * @param mixed $level either an int or a string denoting log level
243: *
244: * @return string log level as string
245: * @since Taurus 1
246: */
247: public function normalizeLogLevel( $level ) {
248:
249: $date = date( 'H:i:s', time() );
250:
251: // If the level is okay, then just return it
252: if ( isset( $this->logLevels[ $level ] )
253: || in_array( $level, $this->logLevels ) ) {
254: return $level;
255: }
256:
257: // the level is invalid; log a message to the console
258: file_put_contents( 'php://stderr', "[{$date}] " .
259: "[{$this->file},{$this->line}] [WARNING] Log level '{$level}' " .
260: "is not valid. Falling back to 'INFO' (1)" . PHP_EOL );
261:
262: // set level to info
263: return 'INFO';
264: }
265:
266: /**
267: * Writes a message to the console (STDERR)
268: *
269: * @param string $message message to log
270: * @since Taurus 1
271: */
272: public function logConsole( $message ) {
273: $date = date( 'H:i:s', time() );
274: file_put_contents( 'php://stderr', "[{$date}] " .
275: "[{$this->file}:{$this->line}] [{$this->level}] {$message}" . PHP_EOL );
276: }
277:
278: /**
279: * Writes message to log file
280: *
281: * @param string $message message to log
282: * @since Taurus 1
283: */
284: public function logFile( $message ) {
285: $date = date( "Y-m-d H:i:s" );
286: $message = "[{$date}] [{$this->file}:{$this->line}] " .
287: "[{$this->level}] ". $message . PHP_EOL;
288: file_put_contents( $this->log, $message, FILE_APPEND | LOCK_EX );
289: }
290:
291:
292: }
293: endif;