Skip to content

Commit f437f6b

Browse files
committed
Add custom log provider option
1 parent bd2d46d commit f437f6b

13 files changed

+188
-1
lines changed

app/src/Tombstone/LogProvider.php

+21
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
namespace Scheb\Tombstone\TestApplication\Tombstone;
6+
7+
use Scheb\Tombstone\Analyzer\Cli\ConsoleOutputInterface;
8+
use Scheb\Tombstone\Analyzer\Log\LogProviderInterface;
9+
10+
class LogProvider implements LogProviderInterface
11+
{
12+
public static function create(array $config, ConsoleOutputInterface $consoleOutput): LogProviderInterface
13+
{
14+
return new self();
15+
}
16+
17+
public function getVampires(): iterable
18+
{
19+
return [];
20+
}
21+
}

app/tombstone.yml

+3
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,9 @@ tombstones:
1111
- "*.js"
1212
logs:
1313
directory: logs
14+
custom:
15+
file: "src/Tombstone/LogProvider.php"
16+
class: 'Scheb\Tombstone\TestApplication\Tombstone\LogProvider'
1417
report:
1518
php: report/tombstone-report.php
1619
checkstyle: report/checkstyle.xml

doc/analyzer/configuration.md

+8
Original file line numberDiff line numberDiff line change
@@ -48,6 +48,14 @@ logs:
4848
# including sub-directories will be processed.
4949
directory: logs
5050

51+
# Alternatively, if you can't write/read logs in the analyzer file format, you can configure a
52+
# custom log provider, that allows you read read logs from any source in any format.
53+
custom:
54+
class: Acme\Tombstone\CustomLogProvider
55+
56+
# Optional, in case the autoloader doesn't automatically find the class file
57+
file: src/tombstone/CustomLogProvider.php
58+
5159
# Report generation options. See the "Report Formats" documentation for more details on this.
5260
report:
5361
php: report/tombstone-report.php # Generate a PHP dump of the result in this file

doc/analyzer/index.md

+1
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ Index
77
-----
88

99
- [Installation](installation.md)
10+
- [Custom Log Providers](log_providers.md)
1011
- [Configuration Reference](configuration.md)
1112
- [Report Formats](report_formats.md)
1213

doc/analyzer/log_providers.md

+59
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,59 @@
1+
Custom Log Providers
2+
====================
3+
4+
You may not want to use the default analyzer log format, but instead write logs in a custom format to a custom logging
5+
system. In the tombstone logger package this is supported by implementing a custom log handler.
6+
7+
The analyzer requires a "log provider" to read log data from your custom log storage. Implement a class with the
8+
interface `Scheb\Tombstone\Analyzer\Log\LogProviderInterface` for that purpose:
9+
10+
```php
11+
<?php
12+
namespace Acme\Tombstone;
13+
14+
use Scheb\Tombstone\Analyzer\Cli\ConsoleOutputInterface;
15+
use Scheb\Tombstone\Analyzer\Log\LogProviderInterface;
16+
use Scheb\Tombstone\Core\Model\Vampire;
17+
18+
class LogProvider implements LogProviderInterface
19+
{
20+
/**
21+
* @param array $config All config options from the YAML file. Additional config options are passed through as-is.
22+
* @param ConsoleOutputInterface $consoleOutput Can be used to write output to the console.
23+
*/
24+
public static function create(array $config, ConsoleOutputInterface $consoleOutput): LogProviderInterface
25+
{
26+
return new self();
27+
}
28+
29+
/**
30+
* Must return an iterable (array or \Traversable) of Vampire objects.
31+
*
32+
* @return iterable<int, Vampire>
33+
*/
34+
public function getVampires(): iterable
35+
{
36+
// Here goes the logic to retrieve log data
37+
}
38+
}
39+
```
40+
41+
The static `create()` function is there to create an instance of your log provider. You can read configuration data from
42+
the YAML configuration via the `$config` array. Any additional config options from that file, that aren't used by the
43+
analyzer, are passed through as-is, allowing you to pass custom configuration to your implementation.
44+
45+
`getVampires` is the method to retrieve the tombstone log data from your logging system. It has to be an iterable
46+
(`array` or `\Traversable`) of `Scheb\Tombstone\Core\Model\Vampire` objects.
47+
48+
Once you have implemented your custom log provider, configure it in the analyzer's YAML config file:
49+
50+
```yaml
51+
logs:
52+
custom:
53+
class: Acme\Tombstone\CustomLogProvider
54+
55+
# Optional, in case the autoloader doesn't automatically find the class file
56+
file: src/tombstone/CustomLogProvider.php
57+
```
58+
59+
When you have a custom log provider configured, it is no longer necessary to have a logs `directory` configured.

doc/logger/handlers_formatters.md

+5
Original file line numberDiff line numberDiff line change
@@ -80,6 +80,11 @@ use Scheb\Tombstone\Logger\Handler\AnalyzerLogHandler;
8080
$analyzerLogHandler = new AnalyzerLogHandler('logs/tombstones', 102400);
8181
```
8282

83+
In some environments it may not be possible to use the `AnalyzerLogHandler` to write tombstone logs, as it depends on
84+
the file system. In such a case it's recommended to implement a custom handler to write tombstone logs to a log storage
85+
of your choice. Then, in order to generate a report with the analyzer, implement a
86+
[custom log provider](../analyzer/log_providers.md) class.
87+
8388
Formatters
8489
----------
8590

src/analyzer/Cli/AnalyzeCommand.php

+16
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@
99
use Scheb\Tombstone\Analyzer\Config\YamlConfigProvider;
1010
use Scheb\Tombstone\Analyzer\Log\AnalyzerLogProvider;
1111
use Scheb\Tombstone\Analyzer\Log\LogCollector;
12+
use Scheb\Tombstone\Analyzer\Log\LogProviderInterface;
1213
use Scheb\Tombstone\Analyzer\Matching\MethodNameStrategy;
1314
use Scheb\Tombstone\Analyzer\Matching\PositionStrategy;
1415
use Scheb\Tombstone\Analyzer\Matching\Processor;
@@ -106,6 +107,21 @@ private function createLogCollector(array $config, VampireIndex $vampireIndex):
106107
if (isset($config['logs']['directory'])) {
107108
$logProviders[] = AnalyzerLogProvider::create($config, $this->output);
108109
}
110+
if (isset($config['logs']['custom'])) {
111+
if (isset($config['logs']['custom']['file'])) {
112+
/** @psalm-suppress UnresolvableInclude */
113+
require_once $config['logs']['custom']['file'];
114+
}
115+
116+
$reflectionClass = new \ReflectionClass($config['logs']['custom']['class']);
117+
if (!$reflectionClass->implementsInterface(LogProviderInterface::class)) {
118+
throw new \Exception(sprintf('Class %s must implement %s', $config['logs']['custom']['class'], LogProviderInterface::class));
119+
}
120+
121+
/** @var LogProviderInterface $logReader */
122+
$logReader = $reflectionClass->newInstance();
123+
$logProviders[] = $logReader;
124+
}
109125

110126
return new LogCollector($logProviders, $vampireIndex);
111127
}

src/analyzer/Config/Configuration.php

+23-1
Original file line numberDiff line numberDiff line change
@@ -66,13 +66,26 @@ public function getConfigTreeBuilder()
6666
->isRequired()
6767
->children()
6868
->scalarNode('directory')
69-
->isRequired()
7069
->cannotBeEmpty()
7170
->validate()
7271
->ifTrue($this->isNoDirectory())
7372
->thenInvalid('Must be a valid directory path, given: %s')
7473
->end()
7574
->end()
75+
->arrayNode('custom')
76+
->children()
77+
->scalarNode('file')
78+
->validate()
79+
->ifTrue($this->isNoFile())
80+
->thenInvalid('Must be a valid file path, given: %s')
81+
->end()
82+
->end()
83+
->scalarNode('class')
84+
->isRequired()
85+
->cannotBeEmpty()
86+
->end()
87+
->end()
88+
->end()
7689
->end()
7790
->end()
7891
->arrayNode('report')
@@ -108,6 +121,15 @@ public function getConfigTreeBuilder()
108121
return $treeBuilder;
109122
}
110123

124+
private function isNoFile(): callable
125+
{
126+
return function (string $path): bool {
127+
$path = realpath($path);
128+
129+
return !(false !== $path && is_file($path));
130+
};
131+
}
132+
111133
private function isNoDirectory(): callable
112134
{
113135
return function (string $path): bool {

src/analyzer/Config/YamlConfigProvider.php

+4
Original file line numberDiff line numberDiff line change
@@ -40,6 +40,10 @@ public function readConfiguration(): array
4040
$config['logs']['directory'] = $this->resolvePath($config['logs']['directory']);
4141
}
4242

43+
if (isset($config['logs']['custom']['file'])) {
44+
$config['logs']['custom']['file'] = $this->resolvePath($config['logs']['custom']['file']);
45+
}
46+
4347
if (isset($config['report']['php'])) {
4448
$config['report']['php'] = $this->resolvePath($config['report']['php']);
4549
}

src/analyzer/Log/LogProviderInterface.php

+6
Original file line numberDiff line numberDiff line change
@@ -9,9 +9,15 @@
99

1010
interface LogProviderInterface
1111
{
12+
/**
13+
* @param array $config All config options from the YAML file. Additional config options are passed through.
14+
* @param ConsoleOutputInterface $consoleOutput can be used to write output to the console
15+
*/
1216
public static function create(array $config, ConsoleOutputInterface $consoleOutput): self;
1317

1418
/**
19+
* Must return an iterable (array or \Traversable) of Vampire objects.
20+
*
1521
* @return iterable<int, Vampire>
1622
*/
1723
public function getVampires(): iterable;

tests/Analyzer/Config/ConfigurationTest.php

+35
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@ class ConfigurationTest extends TestCase
1515
private const APPLICATION_DIR = self::ROOT_DIR.'/app';
1616
private const REPORT_DIR = self::APPLICATION_DIR.'/report';
1717
private const LOGS_DIR = self::APPLICATION_DIR.'/logs';
18+
private const CUSTOM_LOG_PROVIDER = self::APPLICATION_DIR.'/src/Tombstone/LogProvider.php';
1819

1920
private const FULL_CONFIG = [
2021
'source_code' => [
@@ -29,6 +30,10 @@ class ConfigurationTest extends TestCase
2930
],
3031
'logs' => [
3132
'directory' => self::LOGS_DIR,
33+
'custom' => [
34+
'file' => self::CUSTOM_LOG_PROVIDER,
35+
'class' => 'LogProvider',
36+
],
3237
],
3338
'report' => [
3439
'php' => self::REPORT_DIR.'/report.php',
@@ -170,6 +175,7 @@ public function getConfigTreeBuilder_missingLogNode_throwsException(): void
170175
public function getConfigTreeBuilder_emptyLogDirectory_throwsException(): void
171176
{
172177
$config = self::FULL_CONFIG;
178+
unset($config['logs']['custom']);
173179
$config['logs']['directory'] = '';
174180

175181
$this->expectException(InvalidConfigurationException::class);
@@ -183,13 +189,42 @@ public function getConfigTreeBuilder_emptyLogDirectory_throwsException(): void
183189
public function getConfigTreeBuilder_invalidLogDirectory_throwsException(): void
184190
{
185191
$config = self::FULL_CONFIG;
192+
unset($config['logs']['custom']);
186193
$config['logs']['directory'] = 'invalid';
187194

188195
$this->expectException(InvalidConfigurationException::class);
189196
$this->expectExceptionMessage('Must be a valid directory path, given: "invalid"');
190197
$this->processConfiguration($config);
191198
}
192199

200+
/**
201+
* @test
202+
*/
203+
public function getConfigTreeBuilder_missingCustomLogProviderClass_throwsException(): void
204+
{
205+
$config = self::FULL_CONFIG;
206+
unset($config['logs']['directory']);
207+
unset($config['logs']['custom']['class']);
208+
209+
$this->expectException(InvalidConfigurationException::class);
210+
$this->expectExceptionMessageMatches('/"class".*must be configured/');
211+
$this->processConfiguration($config);
212+
}
213+
214+
/**
215+
* @test
216+
*/
217+
public function getConfigTreeBuilder_customLogProviderInvalidFile_throwsException(): void
218+
{
219+
$config = self::FULL_CONFIG;
220+
unset($config['logs']['directory']);
221+
$config['logs']['custom']['file'] = 'invalid'; // Not a valid file
222+
223+
$this->expectException(InvalidConfigurationException::class);
224+
$this->expectExceptionMessage('Must be a valid file path, given: "invalid"');
225+
$this->processConfiguration($config);
226+
}
227+
193228
/**
194229
* @test
195230
*/

tests/Analyzer/Config/YamlConfigProviderTest.php

+4
Original file line numberDiff line numberDiff line change
@@ -59,6 +59,10 @@ public function processConfiguration_fullConfig_haveAllValuesSet(): void
5959
],
6060
'logs' => [
6161
'directory' => self::CONFIG_DIR.'logs',
62+
'custom' => [
63+
'file' => self::CONFIG_DIR.'src'.DIRECTORY_SEPARATOR.'Tombstone'.DIRECTORY_SEPARATOR.'LogProvider.php',
64+
'class' => 'Scheb\Tombstone\Analyzer\TestApplication\Tombstone\LogProvider',
65+
],
6266
],
6367
'report' => [
6468
'php' => self::CONFIG_DIR.'report'.DIRECTORY_SEPARATOR.'tombstone-report.php',

tests/Analyzer/Config/fixtures/full.yml

+3
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,9 @@ source_code:
88
- "*.js"
99
logs:
1010
directory: logs
11+
custom:
12+
file: "src/Tombstone/LogProvider.php"
13+
class: 'Scheb\Tombstone\Analyzer\TestApplication\Tombstone\LogProvider'
1114
report:
1215
php: report/tombstone-report.php
1316
checkstyle: report/checkstyle.xml

0 commit comments

Comments
 (0)