LogrotateScanner::__construct()   A
last analyzed

Complexity

Conditions 1
Paths 1

Size

Total Lines 35
Code Lines 12

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 0
CRAP Score 2

Importance

Changes 0
Metric Value
cc 1
eloc 12
nc 1
nop 7
dl 0
loc 35
ccs 0
cts 21
cp 0
crap 2
rs 9.8666
c 0
b 0
f 0
1
<?php
2
3
/**
4
 * \AppserverIo\Appserver\Core\Scanner\LogrotateScanner
5
 *
6
 * NOTICE OF LICENSE
7
 *
8
 * This source file is subject to the Open Software License (OSL 3.0)
9
 * that is available through the world-wide-web at this URL:
10
 * http://opensource.org/licenses/osl-3.0.php
11
 *
12
 * PHP version 5
13
 *
14
 * @author    Tim Wagner <[email protected]>
15
 * @copyright 2015 TechDivision GmbH <[email protected]>
16
 * @license   http://opensource.org/licenses/osl-3.0.php Open Software License (OSL 3.0)
17
 * @link      https://github.com/appserver-io/appserver
18
 * @link      http://www.appserver.io
19
 */
20
21
namespace AppserverIo\Appserver\Core\Scanner;
22
23
use AppserverIo\Psr\ApplicationServer\ContextInterface;
24
25
/**
26
 * This is a scanner that watches a flat directory for files that has to
27
 * be rotated.
28
 *
29
 * @author    Tim Wagner <[email protected]>
30
 * @copyright 2015 TechDivision GmbH <[email protected]>
31
 * @license   http://opensource.org/licenses/osl-3.0.php Open Software License (OSL 3.0)
32
 * @link      https://github.com/appserver-io/appserver
33
 * @link      http://www.appserver.io
34
 */
35
class LogrotateScanner extends AbstractScanner
36
{
37
38
    /**
39
     * The maximal possible file size (2Gb). Limited as a precaution due to PHP
40
     * integer type limitation on x86 systems.
41
     *
42
     * Example values are:
43
     *
44
     * 102400 = 100KB
45
     * 1048576 = 1MB
46
     * 2147483647 = 2GB
47
     *
48
     * @var integer
49
     */
50
    const MAX_FILE_SIZE = 1048576;
51
52
    /**
53
     * Placeholder for the "original filename" part of the file format.
54
     *
55
     * @var string
56
     */
57
    const FILENAME_FORMAT_PLACEHOLDER = '{filename}';
58
59
    /**
60
     * Placeholder for the "size iterator" part of the file format.
61
     *
62
     * @var string
63
     */
64
    const SIZE_FORMAT_PLACEHOLDER = '{sizeIterator}';
65
66
    /**
67
     * The interval in seconds we use to scan the directory.
68
     *
69
     * @var integer
70
     */
71
    protected $interval;
72
73
    /**
74
     * A list with extensions of files we want to watch.
75
     *
76
     * @var array
77
     */
78
    protected $extensionsToWatch;
79
80
    /**
81
     * The directory we want to watch.
82
     *
83
     * @var string
84
     */
85
    protected $directory;
86
87
    /**
88
     * Number of maximal files to keep. Older files exceeding this limit
89
     * will be deleted.
90
     *
91
     * @var integer
92
     */
93
    protected $maxFiles;
94
95
    /**
96
     * Maximal size in byte a file might have after rotation gets triggered.
97
     *
98
     * @var integer
99
     */
100
    protected $maxSize;
101
102
    /**
103
     * UNIX timestamp at which the next rotation has to take
104
     * place (if there are no size based rotations before).
105
     *
106
     * @var integer
107
     */
108
    protected $nextRotationDate;
109
110
    /**
111
     * Constructor sets initialContext object per default and calls
112
     * init function to pass other args.
113
     *
114
     * @param \AppserverIo\Psr\ApplicationServer\ContextInterface $initialContext    The initial context instance
115
     * @param string                                              $name              The unique scanner name from the configuration
116
     * @param string                                              $directory         The directory we want to scan
117
     * @param integer                                             $interval          The interval in seconds we want scan the directory
118
     * @param string                                              $extensionsToWatch The comma separeted list with extensions of files we want to watch
119
     * @param integer                                             $maxFiles          The maximal amount of files to keep (0 means unlimited)
120
     * @param integer                                             $maxSize           The maximal size of a log file in byte (limited to a technical max of 2GB)
121
     */
122
    public function __construct(
123
        ContextInterface $initialContext,
124
        $name,
125
        $directory,
126
        $interval = 1,
127
        $extensionsToWatch = '',
128
        $maxFiles = 10,
129
        $maxSize = LogrotateScanner::MAX_FILE_SIZE
130
    ) {
131
132
        // call parent constructor
133
        parent::__construct($initialContext, $name);
134
135
        // initialize the members
136
        $this->interval = $interval;
137
        $this->directory = $directory;
138
        $this->maxFiles = (integer) $maxFiles;
139
140
        // set the maximum size of log files
141
        $this->maxSize = (int) $maxSize;
142
143
        // pre-initialize the filename format
144
        $this->filenameFormat =
0 ignored issues
show
Bug Best Practice introduced by
The property filenameFormat does not exist. Although not strictly required by PHP, it is generally a best practice to declare properties explicitly.
Loading history...
145
            LogrotateScanner::FILENAME_FORMAT_PLACEHOLDER . '.' .
146
            LogrotateScanner::SIZE_FORMAT_PLACEHOLDER;
147
148
        // explode the comma separated list of file extensions
149
        $this->extensionsToWatch = explode(',', str_replace(' ', '', $extensionsToWatch));
150
151
        // next rotation date is tomorrow
152
        $tomorrow = new \DateTime('tomorrow');
153
        $this->nextRotationDate = $tomorrow->getTimestamp();
154
155
        // immediately start the scanner
156
        $this->start();
157
    }
158
159
    /**
160
     * Returns the interval in seconds we want to scan the directory.
161
     *
162
     * @return integer The interval in seconds
163
     */
164
    protected function getInterval()
165
    {
166
        return $this->interval;
167
    }
168
169
    /**
170
     * Returns the path to the deployment directory
171
     *
172
     * @return \SplFileInfo The deployment directory
173
     */
174
    public function getDirectory()
175
    {
176
        return new \SplFileInfo($this->getService()->getBaseDirectory($this->directory));
177
    }
178
179
    /**
180
     * Returns an array with file extensions that should be
181
     * watched for new deployments.
182
     *
183
     * @return array The array with the file extensions
184
     */
185
    protected function getExtensionsToWatch()
186
    {
187
        return $this->extensionsToWatch;
188
    }
189
190
    /**
191
     * Getter for the file format to store the logfiles under.
192
     *
193
     * @return string The file format to store the logfiles under
194
     */
195
    protected function getFilenameFormat()
196
    {
197
        return $this->filenameFormat;
198
    }
199
200
    /**
201
     * Getter for the maximal size in bytes a log file might have after rotation
202
     * gets triggered.
203
     *
204
     * @return integer The maximal file size in bytes
205
     */
206
    protected function getMaxSize()
207
    {
208
        return $this->maxSize;
209
    }
210
211
    /**
212
     * Getter for the number of maximal files to keep. Older files exceeding this limit
213
     * will be deleted.
214
     *
215
     * @return integer The maximal number of files to keep
216
     */
217
    protected function getMaxFiles()
218
    {
219
        return $this->maxFiles;
220
    }
221
222
    /**
223
     * Setter for UNIX timestamp at which the next rotation has to take.
224
     *
225
     * @param integer $nextRotationDate The next rotation date as UNIX timestamp
226
     *
227
     * @return void
228
     */
229
    protected function setNextRotationDate($nextRotationDate)
230
    {
231
        $this->nextRotationDate = $nextRotationDate;
232
    }
233
234
    /**
235
     * Getter for UNIX timestamp at which the next rotation has to take.
236
     *
237
     * @return string The next rotation date as UNIX timestamp
238
     */
239
    protected function getNextRotationDate()
240
    {
241
        return $this->nextRotationDate;
242
    }
243
244
    /**
245
     * Start the logrotate scanner that queries whether the configured
246
     * log files has to be rotated or not.
247
     *
248
     * @return void
249
     * @see \AppserverIo\Appserver\Core\AbstractThread::main()
250
     */
251
    public function main()
252
    {
253
254
        // load the interval we want to scan the directory
255
        $interval = $this->getInterval();
256
257
        // load the configured directory
258
        $directory = $this->getDirectory();
259
260
        // prepare the extensions of the file we want to watch
261
        $extensionsToWatch = sprintf('{%s}', implode(',', $this->getExtensionsToWatch()));
262
263
        // log the configured deployment directory
264
        $this->getSystemLogger()->info(
265
            sprintf('Start scanning directory %s for files to be rotated (interval %d)', $directory, $interval)
266
        );
267
268
        // watch the configured directory
269
        while (true) {
270
            // clear the filesystem cache
271
            clearstatcache();
272
273
            // iterate over the files to be watched
274
            foreach (glob($directory . '/*.' . $extensionsToWatch, GLOB_BRACE) as $fileToRotate) {
275
                // log that we're rotate the file
276
                $this->getSystemLogger()->debug(
277
                    sprintf('Query wheter it is necessary to rotate %s', $fileToRotate)
278
                );
279
280
                // handle file rotation
281
                $this->handle($fileToRotate);
282
283
                // cleanup files
284
                $this->cleanup($fileToRotate);
285
            }
286
287
            // sleep a while
288
            sleep($interval);
289
        }
290
    }
291
292
    /**
293
     * Will return a glob pattern with which log files belonging to the currently rotated
294
     * file can be found.
295
     *
296
     * @param string $fileToRotate  The file to be rotated
297
     * @param string $fileExtension The file extension
298
     *
299
     * @return string
300
     */
301
    protected function getGlobPattern($fileToRotate, $fileExtension = '')
302
    {
303
304
        // load the file information
305
        $dirname = pathinfo($fileToRotate, PATHINFO_DIRNAME);
306
        $filename = pathinfo($fileToRotate, PATHINFO_FILENAME);
307
308
        // create a glob expression to find all log files
309
        $glob = str_replace(
310
            array(LogrotateScanner::FILENAME_FORMAT_PLACEHOLDER, LogrotateScanner::SIZE_FORMAT_PLACEHOLDER),
311
            array($filename, '[0-9]'),
312
            $dirname . '/' . $this->getFilenameFormat()
313
        );
314
315
        // append the file extension if available
316
        if (empty($fileExtension) === false) {
317
            $glob .= '.' . $fileExtension;
318
        }
319
320
        // return the glob expression
321
        return $glob;
322
    }
323
324
    /**
325
     * Handles the log message.
326
     *
327
     * @param string $fileToRotate The file to be rotated
328
     *
329
     * @return void
330
     */
331
    protected function handle($fileToRotate)
332
    {
333
334
        // next rotation date is tomorrow
335
        $today = new \DateTime();
336
337
        // do we have to rotate based on the current date or the file's size?
338
        if ($this->getNextRotationDate() < $today->getTimestamp()) {
339
            $this->rotate($fileToRotate);
340
        } elseif (file_exists($fileToRotate) && filesize($fileToRotate) >= $this->getMaxSize()) {
341
            $this->rotate($fileToRotate);
342
        }
343
    }
344
345
    /**
346
     * Does the rotation of the log file which includes updating the currently
347
     * used filename as well as cleaning up the log directory.
348
     *
349
     * @param string $fileToRotate The file to be rotated
350
     *
351
     * @return void
352
     */
353
    protected function rotate($fileToRotate)
354
    {
355
356
        // clear the filesystem cache
357
        clearstatcache();
358
359
        // query whether the file is NOT available anymore or we dont have access to it
360
        if (file_exists($fileToRotate) === false ||
361
            is_writable($fileToRotate) === false) {
362
            return;
363
        }
364
365
        // query whether the file has any content, because we don't want to rotate empty files
366
        if (filesize($fileToRotate) === 0) {
367
            return;
368
        }
369
370
        // load the existing log files
371
        $logFiles = glob($this->getGlobPattern($fileToRotate, 'gz'));
372
373
        // sorting the files by name to remove the older ones
374
        usort(
375
            $logFiles,
0 ignored issues
show
Bug introduced by
It seems like $logFiles can also be of type false; however, parameter $array of usort() does only seem to accept array, maybe add an additional type check? ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

375
            /** @scrutinizer ignore-type */ $logFiles,
Loading history...
376
            function ($a, $b) {
377
                return strcmp($b, $a);
378
            }
379
        );
380
381
        // load the information about the found file
382
        $dirname = pathinfo($fileToRotate, PATHINFO_DIRNAME);
383
        $filename = pathinfo($fileToRotate, PATHINFO_FILENAME);
384
385
        // raise the counter of the rotated files
386
        foreach ($logFiles as $fileToRename) {
387
            // load the information about the found file
388
            $extension = pathinfo($fileToRename, PATHINFO_EXTENSION);
389
            $basename = pathinfo($fileToRename, PATHINFO_BASENAME);
390
391
            // prepare the regex to grep the counter with
392
            $regex = sprintf('/^%s\.([0-9]{1,})\.%s/', $filename, $extension);
393
394
            // initialize the counter for the regex result
395
            $counter = array();
396
397
            // check the counter
398
            if (preg_match($regex, $basename, $counter)) {
399
                // load and raise the counter by one
400
                $raised = ((integer) end($counter)) + 1;
401
402
                // prepare the new filename
403
                $newFilename = sprintf('%s/%s.%d.%s', $dirname, $filename, $raised, $extension);
404
405
                // rename the file
406
                rename($fileToRename, $newFilename);
407
            }
408
        }
409
410
        // rotate the file
411
        rename($fileToRotate, $newFilename = sprintf('%s/%s.0', $dirname, $filename));
412
413
        // compress the log file
414
        file_put_contents("compress.zlib://$newFilename.gz", file_get_contents($newFilename));
415
416
        // delete the old file
417
        unlink($newFilename);
418
419
        // next rotation date is tomorrow
420
        $tomorrow = new \DateTime('tomorrow');
421
        $this->setNextRotationDate($tomorrow->getTimestamp());
422
    }
423
424
    /**
425
     * Will cleanup log files based on the value set for their maximal number
426
     *
427
     * @param string $fileToRotate The file to be rotated
428
     *
429
     * @return void
430
     */
431
    protected function cleanup($fileToRotate)
432
    {
433
434
        // load the maximum number of files to keep
435
        $maxFiles = $this->getMaxFiles();
436
437
        // skip GC of old logs if files are unlimited
438
        if (0 === $maxFiles) {
439
            return;
440
        }
441
442
        // load the rotated log files
443
        $logFiles = glob($this->getGlobPattern($fileToRotate, 'gz'));
444
445
        // query whether we've the maximum number of files reached
446
        if ($maxFiles >= count($logFiles)) {
0 ignored issues
show
Bug introduced by
It seems like $logFiles can also be of type false; however, parameter $var of count() does only seem to accept Countable|array, maybe add an additional type check? ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

446
        if ($maxFiles >= count(/** @scrutinizer ignore-type */ $logFiles)) {
Loading history...
447
            return;
448
        }
449
450
        // iterate over the files we want to clean-up
451
        foreach (array_slice($logFiles, $maxFiles) as $fileToDelete) {
0 ignored issues
show
Bug introduced by
It seems like $logFiles can also be of type false; however, parameter $array of array_slice() does only seem to accept array, maybe add an additional type check? ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

451
        foreach (array_slice(/** @scrutinizer ignore-type */ $logFiles, $maxFiles) as $fileToDelete) {
Loading history...
452
            unlink($fileToDelete);
453
        }
454
    }
455
}
456