Skip to content

Add util.log.LogConfiguration #12

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 5 commits into from
Aug 24, 2018
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
130 changes: 130 additions & 0 deletions src/main/php/util/log/LogConfiguration.class.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,130 @@
<?php namespace util\log;

use lang\FormatException;
use lang\IllegalArgumentException;
use lang\Throwable;
use lang\XPClass;
use util\PropertyAccess;

/**
* Log configuration from a properties object
*
* ```ini
* [default]
* uses=console|syslog|files
*
* [console]
* class=util.log.ConsoleAppender
* level=ALL
*
* [files]
* class=util.log.FileAppender
* args="/var/log/server.log"
* level=ALL
*
* [syslog]
* class=util.log.SyslogUdpAppender
* args=127.0.0.1|514|server
* level=WARN|ERROR
* ```
*
* @test xp://util.log.unittest.LogConfigurationTest
*/
class LogConfiguration {
private $categories= [];

/**
* Creates a new log configuration from a properties file
*
* @param util.PropertyAccess $properties
* @throws lang.FormatException if the property file contains errors
*/
public function __construct(PropertyAccess $properties) {
foreach ($properties->sections() as $section) {
$cat= new LogCategory($section);
foreach ($this->appendersFor($properties, $section) as $level => $appender) {
$cat->addAppender($appender, $level);
}
$this->categories[$section]= $cat;
}
}

/**
* Returns log appenders for a given property file section
*
* @param util.PropertyAccess $properties
* @param string $section
* @return iterable
* @throws lang.FormatException
*/
private function appendersFor($properties, $section) {
static $names= [
'INFO' => LogLevel::INFO,
'WARN' => LogLevel::WARN,
'ERROR' => LogLevel::ERROR,
'DEBUG' => LogLevel::DEBUG,
'ALL' => LogLevel::ALL,
'NONE' => LogLevel::NONE,
];

// Class
if ($class= $properties->readString($section, 'class', null)) {
try {
$appender= XPClass::forName($class)->newInstance(...$properties->readArray($section, 'args', []));
} catch (Throwable $e) {
throw new FormatException('Class '.$class.' in section "'.$section.'" cannot be instantiated', $e);
}

if ($levels= $properties->readArray($section, 'level', null)) {
$level= LogLevel::NONE;
foreach ($levels as $name) {
if (!isset($names[$name])) {
throw new FormatException('Level '.$name.' in section "'.$section.'" not recognized');
}
$level |= $names[$name];
}
yield $level => $appender;
} else {
yield LogLevel::ALL => $appender;
}
}

// Uses, referencing other section
if ($uses= $properties->readArray($section, 'uses', null)) {
foreach ($uses as $use) {
if (!$properties->hasSection($use)) {
throw new FormatException('Uses in section "'.$section.'" references non-existant section "'.$use.'"');
}
foreach ($this->appendersFor($properties, $use) as $level => $appender) {
yield $level => $appender;
}
}
}
}

/** @return [:util.log.LogCategory] */
public function categories() { return $this->categories; }

/**
* Test whether this configuration provides a log category by its name
*
* @param string $name
* @return bool
*/
public function provides($name) {
return isset($this->categories[$name]);
}

/**
* Return a log category by its name
*
* @param string $name
* @return util.log.LogCategory
* @throws lang.IllegalArgumentException
*/
public function category($name) {
if (isset($this->categories[$name])) return $this->categories[$name];

throw new IllegalArgumentException('No log category "'.$name.'"');
}
}
202 changes: 202 additions & 0 deletions src/test/php/util/log/unittest/LogConfigurationTest.class.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,202 @@
<?php namespace util\log\unittest;

use io\streams\MemoryInputStream;
use lang\FormatException;
use lang\IllegalArgumentException;
use unittest\TestCase;
use util\Objects;
use util\Properties;
use util\log\ConsoleAppender;
use util\log\FileAppender;
use util\log\LogConfiguration;
use util\log\LogLevel;

class LogConfigurationTest extends TestCase {

/**
* Creates a Properties object from a string
*
* @param string $properties
* @return util.Properties
*/
private function properties($properties) {
$p= new Properties(null);
$p->load(new MemoryInputStream(trim($properties)));
return $p;
}

#[@test]
public function can_create() {
new LogConfiguration($this->properties(''));
}

#[@test]
public function categories_for_empty_file() {
$config= new LogConfiguration($this->properties(''));
$this->assertEquals([], $config->categories());
}

#[@test]
public function categories() {
$config= new LogConfiguration($this->properties('
[default]
class=util.log.ConsoleAppender
'));

$this->assertInstanceOf('[:util.log.LogCategory]', $config->categories());
}

#[@test, @values([
# ['default', true],
# ['files', false],
#])]
public function provides($name, $expected) {
$config= new LogConfiguration($this->properties('
[default]
class=util.log.ConsoleAppender
'));
$this->assertEquals($expected, $config->provides($name));
}

#[@test]
public function appender_configured_via_class() {
$config= new LogConfiguration($this->properties('
[default]
class=util.log.ConsoleAppender
'));

$appenders= $config->category('default')->getAppenders();
$this->assertEquals(1, sizeof($appenders), Objects::stringOf($appenders));
}

#[@test]
public function appenders_referenced_via_uses() {
$config= new LogConfiguration($this->properties('
[default]
uses=console|files

[console]
class=util.log.ConsoleAppender

[files]
class=util.log.FileAppender
args=test.log
'));

$appenders= $config->category('default')->getAppenders();
$this->assertEquals(2, sizeof($appenders), Objects::stringOf($appenders));
}

#[@test]
public function uses_can_be_nested() {
$config= new LogConfiguration($this->properties('
[default]
uses=tee

[tee]
class=util.log.ConsoleAppender
uses=syslog|files

[syslog]
class=util.log.SyslogAppender

[files]
class=util.log.FileAppender
args=test.log
'));

$appenders= $config->category('default')->getAppenders();
$this->assertEquals(3, sizeof($appenders), Objects::stringOf($appenders));
}

#[@test, @expect(
# class= FormatException::class,
# withMessage= 'Uses in section "default" references non-existant section "missing"'
#)]
public function uses_referencing_non_existant_section() {
new LogConfiguration($this->properties('
[default]
uses=console|missing

[console]
class=util.log.ConsoleAppender
'));
}

#[@test, @expect(
# class= FormatException::class,
# withMessage= 'Class util.log.NonExistantAppender in section "default" cannot be instantiated'
#)]
public function non_existant_appender() {
new LogConfiguration($this->properties('
[default]
class=util.log.NonExistantAppender
'));
}

#[@test, @expect(
# class= FormatException::class,
# withMessage= 'Class util.log.ConsoleAppender in section "default" cannot be instantiated'
#)]
public function exceptions_when_instantiating_appenders() {
new LogConfiguration($this->properties('
[default]
class=util.log.ConsoleAppender
args=STDIN
'));
}

#[@test, @expect(
# class= FormatException::class,
# withMessage= 'Level TEST in section "default" not recognized'
#)]
public function non_existant_level() {
new LogConfiguration($this->properties('
[default]
class=util.log.ConsoleAppender
level=TEST
'));
}

#[@test, @expect(
# class= IllegalArgumentException::class,
# withMessage= 'No log category "default"'
#)]
public function missing_category() {
$config= new LogConfiguration($this->properties(''));
$config->category('default');
}

#[@test]
public function category_with_class_and_argument() {
$config= new LogConfiguration($this->properties('
[default]
class=util.log.FileAppender
args=test.log
'));

$appenders= $config->category('default')->getAppenders();
$this->assertEquals('test.log', $appenders[0]->filename);
}

#[@test]
public function categories_with_loglevels() {
$config= new LogConfiguration($this->properties('
[default]
uses=console|files

[console]
class=util.log.ConsoleAppender
level=INFO

[files]
class=util.log.FileAppender
args=test.log
level=ERROR
'));

$cat= $config->category('default');
$this->assertInstanceOf(ConsoleAppender::class, $cat->getAppenders(LogLevel::INFO)[0]);
$this->assertInstanceOf(FileAppender::class, $cat->getAppenders(LogLevel::ERROR)[0]);
}
}