Completed
Pull Request — master (#218)
by Thomas
05:43
created

LogfileCommand::getLogger()   A

Complexity

Conditions 2
Paths 2

Size

Total Lines 8
Code Lines 4

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 0
CRAP Score 6

Importance

Changes 0
Metric Value
dl 0
loc 8
ccs 0
cts 4
cp 0
rs 9.4285
c 0
b 0
f 0
cc 2
eloc 4
nc 2
nop 1
crap 6
1
<?php
2
declare(strict_types = 1);
3
4
namespace BrowscapPHP\Command;
5
6
use BrowscapPHP\Browscap;
7
use BrowscapPHP\Exception\InvalidArgumentException;
8
use BrowscapPHP\Exception\ReaderException;
9
use BrowscapPHP\Exception\UnknownBrowserException;
10
use BrowscapPHP\Exception\UnknownBrowserTypeException;
11
use BrowscapPHP\Exception\UnknownDeviceException;
12
use BrowscapPHP\Exception\UnknownEngineException;
13
use BrowscapPHP\Exception\UnknownPlatformException;
14
use BrowscapPHP\Helper\Filesystem;
15
use BrowscapPHP\Helper\LoggerHelper;
16
use BrowscapPHP\Util\Logfile\ReaderCollection;
17
use BrowscapPHP\Util\Logfile\ReaderFactory;
18
use Doctrine\Common\Cache\FilesystemCache;
19
use Psr\Log\LoggerInterface;
20
use Psr\SimpleCache\CacheInterface;
21
use Roave\DoctrineSimpleCache\SimpleCacheAdapter;
22
use Symfony\Component\Console\Command\Command;
23
use Symfony\Component\Console\Input\InputArgument;
24
use Symfony\Component\Console\Input\InputInterface;
25
use Symfony\Component\Console\Input\InputOption;
26
use Symfony\Component\Console\Output\OutputInterface;
27
use Symfony\Component\Filesystem\Exception\IOException;
28
use Symfony\Component\Finder\Finder;
29
use Symfony\Component\Finder\SplFileInfo;
30
31
/**
32
 * Commands to parse a log file and parse the useragents in it
33
 */
34
class LogfileCommand extends Command
35
{
36
    /**
37
     * @var array
38
     */
39
    private $undefinedClients = [];
40
41
    /**
42
     * @var array
43
     */
44
    private $uas = [];
45
46
    /**
47
     * @var array
48
     */
49
    private $uasWithType = [];
50
51
    /**
52
     * @var int
53
     */
54
    private $countOk = 0;
55
56
    /**
57
     * @var int
58
     */
59
    private $countNok = 0;
60
61
    /**
62
     * @var int
63
     */
64
    private $totalCount = 0;
65
66
    /**
67
     * @var ?CacheInterface
68
     */
69
    private $cache;
70
71
    /**
72
     * @var string
73
     */
74
    private $defaultCacheFolder;
75
76
    /**
77
     * @var LoggerInterface
78
     */
79
    private $logger;
80
81 1
    public function __construct(
82
        string $defaultCacheFolder,
83
        ?CacheInterface $cache = null,
84
        ?LoggerInterface $logger = null
85
    ) {
86 1
        $this->defaultCacheFolder = $defaultCacheFolder;
87 1
        $this->cache = $cache;
88 1
        $this->logger = $logger;
89
90 1
        parent::__construct();
91 1
    }
92
93 1
    protected function configure() : void
94
    {
95
        $this
96 1
            ->setName('browscap:log')
97 1
            ->setDescription('Parses the supplied webserver log file.')
98 1
            ->addArgument(
99 1
                'output',
100 1
                InputArgument::REQUIRED,
101 1
                'Path to output log file',
102 1
                null
103
            )
104 1
            ->addOption(
105 1
                'log-file',
106 1
                'f',
107 1
                InputOption::VALUE_REQUIRED,
108 1
                'Path to a webserver log file'
109
            )
110 1
            ->addOption(
111 1
                'log-dir',
112 1
                'd',
113 1
                InputOption::VALUE_REQUIRED,
114 1
                'Path to webserver log directory'
115
            )
116 1
            ->addOption(
117 1
                'include',
118 1
                'i',
119 1
                InputOption::VALUE_IS_ARRAY | InputOption::VALUE_REQUIRED,
120 1
                'Include glob expressions for log files in the log directory',
121 1
                ['*.log', '*.log*.gz', '*.log*.bz2']
122
            )
123 1
            ->addOption(
124 1
                'exclude',
125 1
                'e',
126 1
                InputOption::VALUE_IS_ARRAY | InputOption::VALUE_REQUIRED,
127 1
                'Exclude glob expressions for log files in the log directory',
128 1
                ['*error*']
129
            )
130 1
            ->addOption(
131 1
                'debug',
132 1
                null,
133 1
                InputOption::VALUE_NONE,
134 1
                'Should the debug mode entered?'
135
            )
136 1
            ->addOption(
137 1
                'cache',
138 1
                'c',
139 1
                InputOption::VALUE_OPTIONAL,
140 1
                'Where the cache files are located',
141 1
                $this->defaultCacheFolder
142
            );
143 1
    }
144
145
    protected function execute(InputInterface $input, OutputInterface $output) : void
146
    {
147
        if (! $input->getOption('log-file') && ! $input->getOption('log-dir')) {
148
            throw InvalidArgumentException::oneOfCommandArguments('log-file', 'log-dir');
149
        }
150
151
        $logger = $this->getLogger($input);
152
153
        $browscap = new Browscap($this->getCache($input), $logger);
154
        $collection = ReaderFactory::factory();
155
        $fs = new Filesystem();
156
157
        /** @var $file \Symfony\Component\Finder\SplFileInfo */
158
        foreach ($this->getFiles($input) as $file) {
159
            $this->uas = [];
160
            $path = $this->getPath($file);
161
162
            $this->countOk = 0;
163
            $this->countNok = 0;
164
165
            $logger->info('Analyzing file "' . $file->getPathname() . '"');
166
167
            $lines = file($path);
168
169
            if (empty($lines)) {
170
                $logger->info('Skipping empty file "' . $file->getPathname() . '"');
171
                continue;
172
            }
173
174
            $this->totalCount = count($lines);
175
176
            foreach ($lines as $line) {
177
                $this->handleLine(
178
                    $output,
179
                    $collection,
180
                    $browscap,
181
                    $line
182
                );
183
            }
184
185
            $this->outputProgress($output, '', true);
186
187
            arsort($this->uas, SORT_NUMERIC);
188
189
            try {
190
                $fs->dumpFile(
191
                    $input->getArgument('output') . '/output.txt',
192
                    implode(PHP_EOL, array_unique($this->undefinedClients))
193
                );
194
            } catch (IOException $e) {
195
                // do nothing
196
            }
197
198
            try {
199
                $fs->dumpFile(
200
                    $input->getArgument('output') . '/output-with-amount.txt',
201
                    $this->createAmountContent()
202
                );
203
            } catch (IOException $e) {
204
                // do nothing
205
            }
206
207
            try {
208
                $fs->dumpFile(
209
                    $input->getArgument('output') . '/output-with-amount-and-type.txt',
210
                    $this->createAmountTypeContent()
211
                );
212
            } catch (IOException $e) {
213
                // do nothing
214
            }
215
        }
216
217
        $outputFile = $input->getArgument('output') . '/output.txt';
218
219
        try {
220
            $fs->dumpFile(
221
                $outputFile,
222
                implode(PHP_EOL, array_unique($this->undefinedClients))
223
            );
224
        } catch (IOException $e) {
225
            throw new \UnexpectedValueException('writing to file "' . $outputFile . '" failed', 0, $e);
226
        }
227
228
        try {
229
            $fs->dumpFile(
230
                $input->getArgument('output') . '/output-with-amount.txt',
231
                $this->createAmountContent()
232
            );
233
        } catch (IOException $e) {
234
            // do nothing
235
        }
236
237
        try {
238
            $fs->dumpFile(
239
                $input->getArgument('output') . '/output-with-amount-and-type.txt',
240
                $this->createAmountTypeContent()
241
            );
242
        } catch (IOException $e) {
243
            // do nothing
244
        }
245
    }
246
247
    private function createAmountContent() : string
248
    {
249
        $counts = [];
250
251
        foreach ($this->uasWithType as $uas) {
252
            foreach ($uas as $userAgentString => $count) {
253
                if (isset($counts[$userAgentString])) {
254
                    $counts[$userAgentString] += $count;
255
                } else {
256
                    $counts[$userAgentString] = $count;
257
                }
258
            }
259
        }
260
261
        $content = '';
262
263
        arsort($counts, SORT_NUMERIC);
264
265
        foreach ($counts as $agentOfLine => $count) {
266
            $content .= "$count\t$agentOfLine\n";
267
        }
268
269
        return $content;
270
    }
271
272
    private function createAmountTypeContent() : string
273
    {
274
        $content = '';
275
        $types = ['B', 'T', 'P', 'D', 'N', 'U'];
276
277
        foreach ($types as $type) {
278
            if (! isset($this->uasWithType[$type])) {
279
                continue;
280
            }
281
282
            arsort($this->uasWithType[$type], SORT_NUMERIC);
283
284
            foreach ($this->uasWithType[$type] as $agentOfLine => $count) {
285
                $content .= "$type\t$count\t$agentOfLine\n";
286
            }
287
        }
288
289
        return $content;
290
    }
291
292
    private function handleLine(
293
        OutputInterface $output,
294
        ReaderCollection $collection,
295
        Browscap $browscap,
296
        string $line
297
    ) : void {
298
        $userAgentString = '';
299
300
        try {
301
            $userAgentString = $collection->read($line);
302
303
            try {
304
                $this->getResult($browscap->getBrowser($userAgentString));
305
            } catch (\Exception $e) {
306
                $this->undefinedClients[] = $userAgentString;
307
308
                throw $e;
309
            }
310
311
            $type = '.';
312
            ++$this->countOk;
313
        } catch (ReaderException $e) {
314
            $type = 'E';
315
            ++$this->countNok;
316
        } catch (UnknownBrowserTypeException $e) {
317
            $type = 'T';
318
            ++$this->countNok;
319
        } catch (UnknownBrowserException $e) {
320
            $type = 'B';
321
            ++$this->countNok;
322
        } catch (UnknownPlatformException $e) {
323
            $type = 'P';
324
            ++$this->countNok;
325
        } catch (UnknownDeviceException $e) {
326
            $type = 'D';
327
            ++$this->countNok;
328
        } catch (UnknownEngineException $e) {
329
            $type = 'N';
330
            ++$this->countNok;
331
        } catch (\Exception $e) {
332
            $type = 'U';
333
            ++$this->countNok;
334
        }
335
336
        $this->outputProgress($output, $type);
337
338
        // count all useragents
339
        if (isset($this->uas[$userAgentString])) {
340
            ++$this->uas[$userAgentString];
341
        } else {
342
            $this->uas[$userAgentString] = 1;
343
        }
344
345
        if ('.' !== $type && 'E' !== $type) {
346
            // count all undetected useragents grouped by detection error
347
            if (! isset($this->uasWithType[$type])) {
348
                $this->uasWithType[$type] = [];
349
            }
350
351
            if (isset($this->uasWithType[$type][$userAgentString])) {
352
                ++$this->uasWithType[$type][$userAgentString];
353
            } else {
354
                $this->uasWithType[$type][$userAgentString] = 1;
355
            }
356
        }
357
    }
358
359
    private function outputProgress(OutputInterface $output, string $result, bool $end = false) : void
360
    {
361
        if (($this->totalCount % 70) === 0 || $end) {
362
            $formatString = '  %' . strlen($this->countOk) . 'd OK, %' . strlen($this->countNok) . 'd NOK, Summary %'
363
                . strlen($this->totalCount) . 'd';
364
365
            if ($end) {
366
                $result = str_pad($result, 70 - ($this->totalCount % 70), ' ', STR_PAD_RIGHT);
367
            }
368
369
            $endString = sprintf($formatString, $this->countOk, $this->countNok, $this->totalCount);
370
371
            $output->writeln($result . $endString);
372
373
            return;
374
        }
375
376
        $output->write($result);
377
    }
378
379
    private function getResult(\stdClass $result) : string
380
    {
381
        if ('Default Browser' === $result->browser) {
382
            throw new UnknownBrowserException('Unknown browser found');
383
        }
384
385
        if ('unknown' === $result->browser_type) {
386
            throw new UnknownBrowserTypeException('Unknown browser type found');
387
        }
388
389
        if (in_array($result->browser_type, ['Bot/Crawler', 'Library'])) {
390
            return '.';
391
        }
392
393
        if ('unknown' === $result->platform) {
394
            throw new UnknownPlatformException('Unknown platform found');
395
        }
396
397
        if ('unknown' === $result->device_type) {
398
            throw new UnknownDeviceException('Unknown device type found');
399
        }
400
401
        if ('unknown' === $result->renderingengine_name) {
402
            throw new UnknownEngineException('Unknown rendering engine found');
403
        }
404
405
        return '.';
406
    }
407
408
    private function getFiles(InputInterface $input) : Finder
409
    {
410
        $finder = Finder::create();
411
412
        if ($input->getOption('log-file')) {
413
            $file = $input->getOption('log-file');
414
            $finder->append(Finder::create()->in(dirname($file))->name(basename($file)));
415
        }
416
417
        if ($input->getOption('log-dir')) {
418
            $dirFinder = Finder::create()
419
                ->in($input->getOption('log-dir'));
420
            array_map([$dirFinder, 'name'], $input->getOption('include'));
421
            array_map([$dirFinder, 'notName'], $input->getOption('exclude'));
422
423
            $finder->append($dirFinder);
424
        }
425
426
        return $finder;
427
    }
428
429
    private function getPath(SplFileInfo $file) : string
430
    {
431
        switch ($file->getExtension()) {
432
            case 'gz':
433
                $path = 'compress.zlib://' . $file->getPathname();
434
                break;
435
            case 'bz2':
436
                $path = 'compress.bzip2://' . $file->getPathname();
437
                break;
438
            default:
439
                $path = $file->getPathname();
440
                break;
441
        }
442
443
        return $path;
444
    }
445
446
    private function getCache(InputInterface $input) : CacheInterface
447
    {
448
        if (null === $this->cache) {
449
            $fileCache = new FilesystemCache($input->getOption('cache'));
450
            $this->cache = new SimpleCacheAdapter($fileCache);
451
        }
452
453
        return $this->cache;
454
    }
455
456
    private function getLogger(InputInterface $input) : LoggerInterface
457
    {
458
        if (null === $this->logger) {
459
            $this->logger = LoggerHelper::createDefaultLogger($input->getOption('debug'));
460
        }
461
462
        return $this->logger;
463
    }
464
}
465