diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml new file mode 100644 index 0000000..802be88 --- /dev/null +++ b/.github/workflows/ci.yml @@ -0,0 +1,59 @@ +# This is a basic workflow to help you get started with Actions + +name: CI + +# Controls when the action will run. +on: + # Triggers the workflow on push or pull request events but only for the master branch + push: + branches: + - master + - main + - FRAMEWORK_6_0 + pull_request: + branches: + - master + - main + - FRAMEWORK_6_0 + + + # Allows you to run this workflow manually from the Actions tab + workflow_dispatch: + +# A workflow run is made up of one or more jobs that can run sequentially or in parallel +jobs: + run: + runs-on: ${{ matrix.operating-system }} + strategy: + matrix: + operating-system: ['ubuntu-20.04'] + php-versions: ['7.4', '8.0', 'latest'] + # phpunit-versions: ['latest', '9.5', '9.0'] # We use assertMatchesRegularExpression, this is not available in 9.0 + phpunit-versions: ['latest', '9.5'] + steps: + - name: Setup github ssh key + run: mkdir -p ~/.ssh/ && ssh-keyscan -t rsa github.com > ~/.ssh/known_hosts + - name: Checkout + uses: actions/checkout@v2 + + - name: Setup PHP + uses: shivammathur/setup-php@v2 + with: + php-version: ${{ matrix.php-versions }} + extensions: bcmath, ctype, curl, dom, gd, fileinfo, gettext, iconv, imagick, json, ldap, mbstring, mysql, opcache, openssl, pcntl, pdo, posix, redis, soap, sockets, sqlite, tokenizer, xmlwriter + ini-values: post_max_size=512M, max_execution_time=360 + coverage: xdebug + tools: php-cs-fixer, phpunit:${{ matrix.phpunit-versions }}, composer:v2, phpstan + - name: Setup Github Token as composer credential + run: composer config -g github-oauth.github.com ${{ secrets.GITHUB_TOKEN }} + - name: Install horde/test dependency and other dependencies + run: | + ## For unclear reasons, github action fails randomly if we do not install before we require. + COMPOSER_ROOT_VERSION=dev-FRAMEWORK_6_0 composer config minimum-stability dev + COMPOSER_ROOT_VERSION=dev-FRAMEWORK_6_0 composer install + - name: Setup problem matchers for PHPUnit + run: echo "::add-matcher::${{ runner.tool_cache }}/phpunit.json" + - name: run phpunit + run: phpunit --bootstrap test/bootstrap.php + - name: run phpstan + run: phpstan analyze src/ --level 8 diff --git a/.github/workflows/phpdoc.yml b/.github/workflows/phpdoc.yml new file mode 100644 index 0000000..f192303 --- /dev/null +++ b/.github/workflows/phpdoc.yml @@ -0,0 +1,69 @@ +# This is a basic workflow to help you get started with Actions + +name: PHPDOC + +# Controls when the action will run. +on: + # Triggers the workflow on push or pull request events but only for the master branch + push: + branches: + - master + - maintaina-composerfixed + - FRAMEWORK_6_0 + # Allows you to run this workflow manually from the Actions tab + workflow_dispatch: + +# A workflow run is made up of one or more jobs that can run sequentially or in parallel +jobs: + run: + runs-on: ubuntu-20.04 + steps: + - name: Setup github ssh key + run: mkdir -p ~/.ssh/ && ssh-keyscan -t rsa github.com > ~/.ssh/known_hosts + - name: Setup uut dir + run: | + export REPO=$(echo "$GITHUB_REPOSITORY" | awk -F / '{print $2}' | sed -e "s/:refs//") + export UUT_DIR=$(pwd) + export WORK_DIR=~ + export BIN_DIR="${WORK_DIR}/bin" + mkdir -p $BIN_DIR + git config --global user.name "PHPDOC CI Job" + git config --global user.email "ci-job@maintaina.com" + - name: Checkout + uses: actions/checkout@v2 + - name: Checkout Doc Dir + uses: actions/checkout@v2 + with: + repository: maintaina/phpdoc + token: ${{secrets.PHPDOC_TOKEN}} + path: "phpdoc-git" + - name: Setup PHP + uses: shivammathur/setup-php@v2 + with: + php-version: 7.4 + extensions: bcmath, ctype, curl, dom, gd, gettext, iconv, imagick, json, ldap, mbstring, mysql, opcache, openssl, pcntl, pdo, posix, redis, soap, sockets, sqlite, tokenizer, xmlwriter + ini-values: post_max_size=512M, max_execution_time=360 + coverage: xdebug + tools: composer:v2 + - name: Setup Github Token as composer credential + run: composer config -g github-oauth.github.com ${{ secrets.GITHUB_TOKEN }} + - name: phpdocumentor run + run: | + export UUT_DIR=$(pwd) + export REPO=$(echo "$GITHUB_REPOSITORY" | awk -F / '{print $2}' | sed -e "s/:refs//") + export WORK_DIR=/home/runner/ + export BIN_DIR="${WORK_DIR}/bin" + wget https://phpdoc.org/phpDocumentor.phar + mkdir "${WORK_DIR}/phpdoc-out" + mv phpDocumentor.phar $BIN_DIR/phpdocumentor + chmod +x "${BIN_DIR}/phpdocumentor" + echo "Creating UUT related dir in docu repo" + mkdir -p $UUT_DIR/phpdoc-git/${GITHUB_REF##*/}/${REPO}/ + ## TODO: check for and include lib, src, app (if they exist) but not test or script dirs + $BIN_DIR/phpdocumentor -d $UUT_DIR/lib/ -d $UUT_DIR/src/ -d $UUT_DIR/app/ -t "${UUT_DIR}/phpdoc-git/${GITHUB_REF##*/}/${REPO}/" + cd ${UUT_DIR}/phpdoc-git + git add "${GITHUB_REF##*/}/${REPO}" + php indexer.php ${GITHUB_REF##*/} $REPO + git add index.html index.json + git commit -m "Updated phpdoc for $GITHUB_REPOSITORY (${GITHUB_REF##*/} branch) from ci" + git push diff --git a/.github/workflows/update-composer-json.yml b/.github/workflows/update-composer-json.yml new file mode 100644 index 0000000..5b73f05 --- /dev/null +++ b/.github/workflows/update-composer-json.yml @@ -0,0 +1,47 @@ +--- +name: update composer.json + +# run whenever we push to the default branch and .horde.yml was changed +on: + push: + branches: + - FRAMEWORK_6_0 + paths: + - '.horde.yml' + +jobs: + run: + runs-on: ${{ matrix.operating-system }} + strategy: + matrix: + operating-system: ['ubuntu-20.04'] + php-versions: ['7.4'] + steps: + - name: Setup git + run: | + mkdir -p ~/.ssh/ && ssh-keyscan -t rsa github.com > ~/.ssh/known_hosts + git config --global user.name "Github CI Runner" + git config --global user.email "ci-job@maintaina.com" + - name: Checkout + uses: actions/checkout@v2 + - name: Setup PHP + uses: shivammathur/setup-php@v2 + with: + php-version: ${{ matrix.php-versions }} + extensions: gettext + ini-values: post_max_size=512M, max_execution_time=360 + tools: composer:v2 + - name: Setup composer + run: | + composer global config github-oauth.github.com ${{ secrets.GITHUB_TOKEN }} + composer global config repositories.0 composer https://horde-satis.maintaina.com + composer global config minimum-stability dev + composer global require horde/components "dev-FRAMEWORK_6_0" + alias horde-components="~/.config/composer/web/components/bin/horde-components" + - name: create new composer.json + run: horde-components -c doc/components.conf composer + - name: push new composer.json + run: | + git add composer.json + git commit -m "automatic generation of composer.json - [ci skip]" + git push diff --git a/.github/workflows/update-satis.yml b/.github/workflows/update-satis.yml new file mode 100644 index 0000000..5049bd0 --- /dev/null +++ b/.github/workflows/update-satis.yml @@ -0,0 +1,59 @@ +# This is a basic workflow to help you get started with Actions + +name: Update Satis + +# Controls when the action will run. +on: + # Triggers the workflow on push or pull request events but only for the master branch + push: + branches: + - FRAMEWORK_6_0 + # Allows you to run this workflow manually from the Actions tab + workflow_dispatch: + +# A workflow run is made up of one or more jobs that can run sequentially or in parallel +jobs: + run: + runs-on: ubuntu-20.04 + steps: + - name: Setup github ssh key + run: mkdir -p ~/.ssh/ && ssh-keyscan -t rsa github.com > ~/.ssh/known_hosts + - name: Setup uut dir + run: | + export REPO=$(echo "$GITHUB_REPOSITORY" | awk -F / '{print $2}' | sed -e "s/:refs//") + export UUT_DIR=$(pwd) + export WORK_DIR=~ + export BIN_DIR="${WORK_DIR}/bin" + mkdir -p $BIN_DIR + git config --global user.name "PHPDOC CI Job" + git config --global user.email "ci-job@maintaina.com" + - name: Checkout + uses: actions/checkout@v2 + - name: Checkout satis web dir + uses: actions/checkout@v2 + with: + repository: maintaina-com/maintaina-com.github.io + token: ${{secrets.PHPDOC_TOKEN}} + path: "maintaina-com.github.io" + - name: Setup PHP + uses: shivammathur/setup-php@v2 + with: + php-version: 7.4 + extensions: bcmath, ctype, curl, dom, gd, gettext, iconv, imagick, json, ldap, mbstring, mysql, opcache, openssl, pcntl, pdo, posix, redis, soap, sockets, sqlite, tokenizer, xmlwriter + ini-values: post_max_size=512M, max_execution_time=3600 + coverage: xdebug + tools: composer:v2 + - name: Setup Github Token as composer credential + run: composer config -g github-oauth.github.com ${{ secrets.GITHUB_TOKEN }} + - name: Run Satis + run: | + export UUT_DIR=$(pwd) + export REPO=$(echo "$GITHUB_REPOSITORY" | awk -F / '{print $2}' | sed -e "s/:refs//") + export WORK_DIR=/home/runner/ + export BIN_DIR="${WORK_DIR}/bin" + composer create-project composer/satis:dev-main + php satis/bin/satis build -vvv maintaina-com.github.io/satis.json maintaina-com.github.io/ horde/${REPO,,} + cd maintaina-com.github.io + git add include/ index.html p2/ packages.json + git commit -m "Update for horde/${REPO,,}" + git push diff --git a/.gitignore b/.gitignore index 4e78ae3..9c23fa4 100644 --- a/.gitignore +++ b/.gitignore @@ -27,3 +27,9 @@ run-tests.log /test/*/*/*/*.log /test/*/*/*/*.out +vendor/ +web/ +.php-cs-fixer.cache +.phpunit.cache +.phpdoc +composer.lock diff --git a/.horde.yml b/.horde.yml index ffccef7..018f03e 100644 --- a/.horde.yml +++ b/.horde.yml @@ -19,8 +19,14 @@ authors: email: chuck@horde.org active: false role: lead + - + name: Ralf Lang + user: lang + email: lang@b1-systems.de + active: true + role: maintainer version: - release: 3.0.0alpha4 + release: 3.0.0alpha6 api: 3.0.0alpha1 state: release: alpha @@ -30,11 +36,18 @@ license: uri: http://www.horde.org/licenses/bsd dependencies: required: - php: ^7 + php: ^7.4 || ^8 composer: + php-extended/polyfill-php80-stringable: ^1 + psr/log: ^1 horde/constraint: ^3 horde/exception: ^3 horde/util: ^3 + dev: + composer: + horde/test: ^3 + horde/cli: ^3 + horde/scribe: ^3 optional: composer: horde/cli: ^3 diff --git a/.travis.yml b/.travis.yml deleted file mode 100644 index 7167e3f..0000000 --- a/.travis.yml +++ /dev/null @@ -1,19 +0,0 @@ -language: php -php: - - 5.6 - - 7.0 - - 7.1 - - 7.2 - - 7.3 - - 7.4 - - nightly -jobs: - fast_finish: true - allow_failures: - - php: nightly -before_script: - - phpenv config-rm xdebug.ini || echo "XDebug not enabled" - - pear install channel://pear.horde.org/Horde_Test - - pear install -a -B package.xml -script: - - php $(pear config-get php_dir)/Horde/Test/vendor/phpunit/phpunit/phpunit diff --git a/composer.json b/composer.json index fc565a1..202e651 100644 --- a/composer.json +++ b/composer.json @@ -1,5 +1,4 @@ { - "minimum-stability": "dev", "name": "horde/log", "description": "Logging library", "type": "library", @@ -15,30 +14,45 @@ "name": "Chuck Hagenbuch", "email": "chuck@horde.org", "role": "lead" - } - ], - "time": "2021-03-13", - "repositories": [ + }, { - "type": "composer", - "url": "https://horde-satis.maintaina.com/" + "name": "Ralf Lang", + "email": "lang@b1-systems.de", + "role": "maintainer" } ], + "time": "2021-11-28", + "repositories": [], "require": { - "php": "^7", - "horde/constraint": "^3 || dev-FRAMEWORK_6_0", - "horde/exception": "^3 || dev-FRAMEWORK_6_0", - "horde/util": "^3 || dev-FRAMEWORK_6_0" + "php": "^7.4 || ^8", + "php-extended/polyfill-php80-stringable": "^1", + "psr/log": "^1", + "horde/constraint": "^3", + "horde/exception": "^3", + "horde/util": "^3" + }, + "require-dev": { + "horde/test": "^3", + "horde/cli": "^3", + "horde/scribe": "^3" }, "suggest": { - "horde/cli": "^3 || dev-FRAMEWORK_6_0", - "horde/scribe": "^3 || dev-FRAMEWORK_6_0", - "horde/test": "^3 || dev-FRAMEWORK_6_0", + "horde/cli": "^3", + "horde/scribe": "^3", + "horde/test": "^3", "ext-dom": "*" }, "autoload": { "psr-0": { "Horde_Log": "lib/" + }, + "psr-4": { + "Horde\\Log\\": "src/" + } + }, + "autoload-dev": { + "psr-4": { + "Horde\\Log\\Test\\": "test/" } } } \ No newline at end of file diff --git a/doc/Horde/Log/CHANGES b/doc/Horde/Log/CHANGES index accbdc7..5ad2af0 100644 --- a/doc/Horde/Log/CHANGES +++ b/doc/Horde/Log/CHANGES @@ -1,8 +1,24 @@ ------------ -v3.0.0alpha4 +v3.0.0alpha6 +------------ + + + + +------------ +v3.0.0alpha5 ------------ +[rla] Turned LogHandler options into classes. +|+ + +------------ +v3.0.0alpha4 +------------ + +[rla] Add Namespaced, PSR-3 compatible new logger design. +|+ ------------ diff --git a/doc/Horde/Log/changelog.yml b/doc/Horde/Log/changelog.yml index 84ed3f5..9a42a43 100644 --- a/doc/Horde/Log/changelog.yml +++ b/doc/Horde/Log/changelog.yml @@ -1,5 +1,5 @@ --- -3.0.0alpha4: +3.0.0alpha6: api: 3.0.0alpha1 state: release: alpha @@ -10,6 +10,30 @@ uri: http://www.horde.org/licenses/bsd notes: |+ +3.0.0alpha5: + api: 3.0.0alpha1 + state: + release: alpha + api: alpha + date: 2021-03-13 + license: + identifier: BSD-2-Clause + uri: http://www.horde.org/licenses/bsd + notes: | + [rla] Turned LogHandler options into classes. + |+ +3.0.0alpha4: + api: 3.0.0alpha1 + state: + release: alpha + api: alpha + date: 2021-03-13 + license: + identifier: BSD-2-Clause + uri: http://www.horde.org/licenses/bsd + notes: | + [rla] Add Namespaced, PSR-3 compatible new logger design. + |+ 3.0.0alpha3: api: 3.0.0alpha1 state: diff --git a/lib/Horde/Log/Handler/Stream.php b/lib/Horde/Log/Handler/Stream.php index e9fe4b3..d078775 100644 --- a/lib/Horde/Log/Handler/Stream.php +++ b/lib/Horde/Log/Handler/Stream.php @@ -94,8 +94,21 @@ public function __construct($streamOrUrl, $mode = 'a+', */ public function __wakeup() { - if (!($this->_stream = @fopen($this->_streamOrUrl, $this->_mode, false))) { - throw new Horde_Log_Exception(__CLASS__ . ': "' . $this->_streamOrUrl . '" cannot be opened with mode "' . $this->_mode . '"'); + $streamOrUrl = $this->_streamOrUrl; + try { + $res = $this->_stream = @fopen($streamOrUrl, $this->_mode, false); + } catch (Throwable $e) { + if (is_resource($streamOrUrl)) { + $streamOrUrl = 'resource'; + } elseif (is_object($streamOrUrl) && !method_exists($streamOrUrl, '__toString')) { + $streamOrUrl = get_class($streamOrUrl); + } else { + $streamOrUrl = (string) $streamOrUrl; + } + throw new Horde_Log_Exception(__CLASS__ . ': "' . $streamOrUrl . '" cannot be opened with mode "' . $this->_mode . '"' . $e->getMessage()); + } + if (!$res) { + throw new Horde_Log_Exception(__CLASS__ . ': "' . $streamOrUrl . '" cannot be opened with mode "' . $this->_mode . '"'); } } @@ -114,10 +127,14 @@ public function write($event) } $line = $this->_formatter->format($event); - if (!@fwrite($this->_stream, $line)) { + try { + $res = @fwrite($this->_stream, $line); + } catch (Throwable $e) { + throw new Horde_Log_Exception(__CLASS__ . ': Unable to write to stream: ' . $this->_mode . '"' . $e->getMessage()); + } + if (!$res) { throw new Horde_Log_Exception(__CLASS__ . ': Unable to write to stream'); } - return true; } diff --git a/package.xml b/package.xml index 3dbd289..ffbee98 100644 --- a/package.xml +++ b/package.xml @@ -5,6 +5,24 @@ Log Logging library A logging library with configurable handlers, filters, and formatting. + + Ralf Lang + lang + lang@b1-systems.de + yes + + + Ralf Lang + lang + lang@b1-systems.de + yes + + + Ralf Lang + lang + lang@b1-systems.de + yes + Mike Naberezny mnaberez @@ -17,9 +35,15 @@ chuck@horde.org no + + Ralf Lang + lang + lang@b1-systems.de + yes + 2021-03-13 - 3.0.0alpha4 + 3.0.0alpha6 3.0.0alpha1 @@ -104,9 +128,9 @@ - 7.0.0 - 8.0.0alpha1 - 8.0.0alpha1 + 7.4.0 + 9.0.0alpha1 + 9.0.0alpha1 1.7.0 @@ -160,27 +184,6 @@ - - - - - - - - - - - - - - - - - - - - - 1.0.0alpha1 @@ -510,6 +513,38 @@ 2021-03-13 BSD-2-Clause +* [rla] Add Namespaced, PSR-3 compatible new logger design. +* |+ + + + + + 3.0.0alpha5 + 3.0.0alpha1 + + + alpha + alpha + + 2021-03-13 + BSD-2-Clause + +* [rla] Turned LogHandler options into classes. +* |+ + + + + + 3.0.0alpha6 + 3.0.0alpha1 + + + alpha + alpha + + 2021-03-13 + BSD-2-Clause + * diff --git a/phpunit.xml.dist b/phpunit.xml.dist index 4611e84..acb854e 100644 --- a/phpunit.xml.dist +++ b/phpunit.xml.dist @@ -1,12 +1,26 @@ - - - - test - - - - - lib - - - \ No newline at end of file + + + + + test + + + + + + lib + + + diff --git a/src/Filter/ConstraintFilter.php b/src/Filter/ConstraintFilter.php new file mode 100644 index 0000000..2c09246 --- /dev/null +++ b/src/Filter/ConstraintFilter.php @@ -0,0 +1,155 @@ + + * @category Horde + * @license http://www.horde.org/licenses/bsd BSD + * @package Log + * @subpackage Filters + */ +declare(strict_types=1); + +namespace Horde\Log\Filter; + +use Horde\Log\LogFilter; +use Horde\Log\LogMessage; +use Horde_Constraint_Coupler; +use Horde_Constraint_PregMatch; +use Horde_Constraint_And; +use Horde_Constraint_Not; +use Horde_Constraint_Null; +use Horde_Constraint; + +/** + * Filters log events using defined constraints on one or more fields of the + * $event array. + * + * @author James Pepin + * @category Horde + * @license http://www.horde.org/licenses/bsd BSD + * @package Log + * @subpackage Filters + * + * @todo Implement constraint objects for the different types of filtering ie + * regex,required,type..etc.. so we can add different constaints ad infinitum. + */ +class ConstraintFilter implements LogFilter +{ + /** + * Constraint list. + * + * @var Horde_Constraint_Coupler[] + */ + protected $constraints = []; + + /** + * Default constraint coupler. + * + * @var Horde_Constraint_Coupler + * @default Horde_Constraint_And + */ + protected $coupler; + + /** + * Constructor. + * + * @param Horde_Constraint_Coupler $coupler The default kind of + * constraint to use to couple + * multiple constraints. + * Defaults to And. + */ + public function __construct(Horde_Constraint_Coupler $coupler = null) + { + $this->coupler = is_null($coupler) + ? new Horde_Constraint_And() + : $coupler; + } + + /** + * Add a constraint to the filter. + * + * @param string $field The field to apply the constraint + * to. + * @param Horde_Constraint $constraint The constraint to apply. + * + * @return ConstraintFilter A reference to $this to allow + * method chaining. + */ + public function addConstraint($field, Horde_Constraint $constraint): self + { + if (!isset($this->constraints[$field])) { + $this->constraints[$field] = clone($this->coupler); + } + $this->constraints[$field]->addConstraint($constraint); + + return $this; + } + + /** + * Add a regular expression to filter by. + * + * Takes a field name and a regex, if the regex does not match then the + * event is filtered. + * + * @param string $field The name of the field that should be part of the + * event. + * @param string $regex The regular expression to filter by. + * @return ConstraintFilter A reference to $this to allow + * method chaining. + */ + public function addRegex($field, $regex): self + { + return $this->addConstraint($field, new Horde_Constraint_PregMatch($regex)); + } + + /** + * Add a required field to the filter. + * + * If the field does not exist on the event, then it is filtered. + * + * @param string $field The name of the field that should be part of the + * event. + * + * @return ConstraintFilter A reference to $this to allow + * method chaining. + */ + public function addRequiredField($field): self + { + return $this->addConstraint($field, new Horde_Constraint_Not(new Horde_Constraint_Null())); + } + + /** + * Adds all arguments passed as required fields. + * + * @return ConstraintFilter A reference to $this to allow + * method chaining. + */ + public function addRequiredFields(): self + { + foreach (func_get_args() as $f) { + $this->addRequiredField($f); + } + + return $this; + } + + /** + * Returns Horde_Log_Filter::ACCEPT to accept the message, + * Horde_Log_Filter::IGNORE to ignore it. + * + * @param LogMessage $event Log event. + * + * @return bool accepted? + */ + public function accept(LogMessage $event): bool + { + $eventArr = array_merge(['message' => $event->message(), 'loglevel' => $event->level()->criticality()], $event->context()); + foreach ($this->constraints as $field => $constraint) { + $value = $eventArr[$field] ?? null; + if (!$constraint->evaluate($value)) { + return LogFilter::IGNORE; + } + } + + return LogFilter::ACCEPT; + } +} diff --git a/src/Filter/ExactLevelFilter.php b/src/Filter/ExactLevelFilter.php new file mode 100644 index 0000000..786ab3c --- /dev/null +++ b/src/Filter/ExactLevelFilter.php @@ -0,0 +1,66 @@ + + * @category Horde + * @license http://www.horde.org/licenses/bsd BSD + * @package Log + * @subpackage Filters + */ +declare(strict_types=1); + +namespace Horde\Log\Filter; + +use Horde\Log\LogFilter; +use Horde\Log\LogMessage; +use Horde\Log\LogLevel; +use Horde\Log\LogException; + +/** + * @author Bryan Alves + * @category Horde + * @license http://www.horde.org/licenses/bsd BSD + * @package Log + * @subpackage Filters + */ +class ExactLevelFilter implements LogFilter +{ + /** + * @var int + */ + protected int $level; + protected ?string $name = null; + + /** + * Filter out any log messages not equal to $level. + * + * @param int $level Log level to pass through the filter + * @param string|null $name optionally also check for same level name + */ + public function __construct(int $level, string $name = null) + { + $this->level = $level; + $this->name = $name; + } + + public static function constructFromLevel(LogLevel $level): self + { + return new self($level->criticality(), $level->name()); + } + + /** + * Returns TRUE to accept the message, FALSE to block it. + * + * @param LogMessage $event Log event + * @return bool accepted? + */ + public function accept(LogMessage $event): bool + { + $loglevel = $event->level(); + if ($this->name && ($this->name != $loglevel->name())) { + return false; + } + return $loglevel->criticality() == $this->level; + } +} diff --git a/src/Filter/MaximumLevelFilter.php b/src/Filter/MaximumLevelFilter.php new file mode 100644 index 0000000..32bdc15 --- /dev/null +++ b/src/Filter/MaximumLevelFilter.php @@ -0,0 +1,63 @@ + + * @author Chuck Hagenbuch + * @category Horde + * @license http://www.horde.org/licenses/bsd BSD + * @package Log + * @subpackage Filters + */ +declare(strict_types=1); + +namespace Horde\Log\Filter; + +use Horde\Log\LogFilter; +use Horde\Log\LogMessage; +use InvalidArgumentException; + +/** + * @author Mike Naberezny + * @author Chuck Hagenbuch + * @category Horde + * @license http://www.horde.org/licenses/bsd BSD + * @package Log + * @subpackage Filters + */ +class MaximumLevelFilter implements LogFilter +{ + /** + * Filter level. + * + * @var int + */ + protected int $level; + + /** + * Filter out any log messages greater than $level. + * + * @param integer $level Maximum log level to pass through the filter. + */ + public function __construct(int $level) + { + $this->level = $level; + } + + /** + * Returns Horde_Log_Filter::ACCEPT to accept the message, + * Horde_Log_Filter::IGNORE to ignore it. + * + * @param LogMessage $event Log event. + * + * @return bool Accepted? + */ + public function accept(LogMessage $event): bool + { + return ($event->level()->criticality() <= $this->level); + } +} diff --git a/src/Filter/MessageFilter.php b/src/Filter/MessageFilter.php new file mode 100644 index 0000000..68365c6 --- /dev/null +++ b/src/Filter/MessageFilter.php @@ -0,0 +1,69 @@ + + * @author Chuck Hagenbuch + * @category Horde + * @license http://www.horde.org/licenses/bsd BSD + * @package Log + * @subpackage Filters + */ +declare(strict_types=1); + +namespace Horde\Log\Filter; + +use Horde\Log\LogFilter; +use Horde\Log\LogMessage; +use InvalidArgumentException; + +/** + * @author Mike Naberezny + * @author Chuck Hagenbuch + * @category Horde + * @license http://www.horde.org/licenses/bsd BSD + * @package Log + * @subpackage Filters + */ +class MessageFilter implements LogFilter +{ + /** + * Filter regex. + * + * @var string + */ + protected string $regexp; + + /** + * Filter out any log messages not matching $regexp. + * + * @param string $regexp Regular expression to test the log message. + * + * @throws InvalidArgumentException Invalid regular expression. + */ + public function __construct(string $regexp) + { + if (@preg_match($regexp, '') === false) { + throw new InvalidArgumentException('Invalid regular expression ' . $regexp); + } + + $this->regexp = $regexp; + } + + /** + * Returns Horde_Log_Filter::ACCEPT to accept the message, + * Horde_Log_Filter::IGNORE to ignore it. + * + * @param LogMessage $event Log event. + * + * @return bool Accepted? + */ + public function accept(LogMessage $event): bool + { + return (preg_match($this->regexp, $event->message()) > 0); + } +} diff --git a/src/Filter/MinimumLevelFilter.php b/src/Filter/MinimumLevelFilter.php new file mode 100644 index 0000000..680b32b --- /dev/null +++ b/src/Filter/MinimumLevelFilter.php @@ -0,0 +1,69 @@ + + * @author Chuck Hagenbuch + * @category Horde + * @license http://www.horde.org/licenses/bsd BSD + * @package Log + * @subpackage Filters + */ +declare(strict_types=1); + +namespace Horde\Log\Filter; + +use Horde\Log\LogFilter; +use Horde\Log\LogMessage; +use Psr\Log\InvalidArgumentException; + +/** + * @author Ralf Lang + * @category Horde + * @license http://www.horde.org/licenses/bsd BSD + * @package Log + * @subpackage Filters + * @since v3.0.0 + */ +class MinimumLevelFilter implements LogFilter +{ + /** + * Filter level. + * + * @var integer + */ + protected int $level; + + /** + * Filter out any log messages more urgent than $level. + * + * @param int $level Maximum log level to pass through the filter. + * + * @throws InvalidArgumentException + */ + public function __construct(int $level) + { + if (!is_integer($level)) { + throw new InvalidArgumentException('Level must be an integer'); + } + + $this->level = $level; + } + + /** + * Returns Horde\Log\Filter::ACCEPT to accept the message, + * Horde\Log\Filter::IGNORE to ignore it. + * + * @param LogMessage $event Log event. + * + * @return bool Accepted? + */ + public function accept(LogMessage $event): bool + { + return ($event->level()->criticality() >= $this->level); + } +} diff --git a/src/Filter/SuppressFilter.php b/src/Filter/SuppressFilter.php new file mode 100644 index 0000000..d38c3ac --- /dev/null +++ b/src/Filter/SuppressFilter.php @@ -0,0 +1,65 @@ + + * @author Chuck Hagenbuch + * @category Horde + * @license http://www.horde.org/licenses/bsd BSD + * @package Log + * @subpackage Filters + */ +declare(strict_types=1); + +namespace Horde\Log\Filter; + +use Horde\Log\LogFilter; +use Horde\Log\LogMessage; + +/** + * @author Mike Naberezny + * @author Chuck Hagenbuch + * @category Horde + * @license http://www.horde.org/licenses/bsd BSD + * @package Log + * @subpackage Filters + */ +class SuppressFilter implements LogFilter +{ + /** + * Accept all events? + * + * @var bool + */ + protected bool $accept = LogFilter::ACCEPT; + + /** + * This is a simple boolean filter. + * + * @param bool $suppress Should all log events be suppressed? + */ + public function suppress($suppress): bool + { + $this->accept = !$suppress; + return $suppress; + } + + /** + * Decide if we accept messages. + * + * Returns Horde\Log\Filter::ACCEPT to accept the message, + * Horde_Log_Filter::IGNORE to ignore it. + * + * @param LogMessage $event Event data. + * + * @return bool Accepted? + */ + public function accept(LogMessage $event): bool + { + return $this->accept; + } +} diff --git a/src/Formatter/CliFormatter.php b/src/Formatter/CliFormatter.php new file mode 100644 index 0000000..bd4cce6 --- /dev/null +++ b/src/Formatter/CliFormatter.php @@ -0,0 +1,87 @@ + + * @category Horde + * @license http://www.horde.org/licenses/bsd BSD + * @package Log + * @subpackage Handlers + */ +declare(strict_types=1); + +namespace Horde\Log\Formatter; + +use Horde\Log\LogFormatter; +use Horde\Log\LogMessage; +use Horde_Cli; +use Horde\Log\LogLevel; + +/** + * Formatter for the command line interface using Horde_Cli. + * + * @author Jan Schneider + * @category Horde + * @license http://www.horde.org/licenses/bsd BSD + * @package Log + * @subpackage Formatters + */ +class CliFormatter implements LogFormatter +{ + /** + * A CLI handler. + * + * @var Horde_Cli + */ + protected $cli; + + /** + * Constructor. + * + * @param Horde_Cli $cli A Horde_Cli instance. + */ + public function __construct(Horde_Cli $cli) + { + $this->cli = $cli; + } + + /** + * Formats an event to be written by the handler. + * + * @param LogMessage $event Log event. + * + * @return string Formatted line. + */ + public function format(LogMessage $event): string + { + $loglevel = $event->level(); + $flag = '['. str_pad($loglevel->name(), 7, ' ', STR_PAD_BOTH) . '] '; + + switch ($loglevel->name()) { + case 'emergency': + case 'alert': + case 'critical': + case 'crit': + case 'error': + case 'err': + $type_message = $this->cli->color('red', $flag); + break; + + case 'warn': + case 'warning': + case 'notice': + $type_message = $this->cli->color('yellow', $flag); + break; + + case 'info': + case 'debug': + $type_message = $this->cli->color('blue', $flag); + break; + + default: + $type_message = $flag; + } + + return $type_message . $event->message(); + } +} diff --git a/src/Formatter/Psr3Formatter.php b/src/Formatter/Psr3Formatter.php new file mode 100644 index 0000000..9949d4c --- /dev/null +++ b/src/Formatter/Psr3Formatter.php @@ -0,0 +1,72 @@ + + * @category Horde + * @license http://www.horde.org/licenses/bsd BSD + * @package Log + * @subpackage Formatters + */ +declare(strict_types=1); + +namespace Horde\Log\Formatter; + +use Horde\Log\LogFormatter; +use Horde\Log\LogMessage; +use InvalidArgumentException; +use Stringable; + +/** + * @author Mike Naberezny + * @author Chuck Hagenbuch + * @category Horde + * @license http://www.horde.org/licenses/bsd BSD + * @package Log + * @subpackage Formatters + */ +class Psr3Formatter implements LogFormatter +{ + /** + * Hash of default context values. + * + * @var string[]|Stringable[]|int[]|float[] + */ + private array $defaultContext; + /** + * Constructor. + * + * @param string[]|Stringable[]|int[]|float[] $defaultContext Defaults hash for missing context values. Key will be the placeholder, value will be the filled in data. Actual context overwrites defaults + * + * + * @throws InvalidArgumentException + */ + public function __construct(array $defaultContext = []) + { + $this->defaultContext = $defaultContext; + } + + /** + * Formats an event to be written by the handler. + * + * @param LogMessage $event Log event. + * + * @return string Formatted line. + */ + public function format(LogMessage $event): string + { + $context = array_merge($this->defaultContext, $event->context()); + $placeholders = []; + foreach ($context as $key => $value) { + // Filter out incompatible objects, arrays etc + if ($value instanceof Stringable || is_string($value) || is_numeric($value)) { + $placeholders['{' . $key . '}'] = (string) $value; + } + } + return strtr($event->formattedMessage(), $placeholders); + } +} diff --git a/src/Formatter/SimpleFormatter.php b/src/Formatter/SimpleFormatter.php new file mode 100644 index 0000000..c9e044d --- /dev/null +++ b/src/Formatter/SimpleFormatter.php @@ -0,0 +1,85 @@ + + * @author Chuck Hagenbuch + * @category Horde + * @license http://www.horde.org/licenses/bsd BSD + * @package Log + * @subpackage Formatters + */ +declare(strict_types=1); + +namespace Horde\Log\Formatter; + +use Horde\Log\LogFormatter; +use Horde\Log\LogMessage; +use InvalidArgumentException; + +/** + * @author Mike Naberezny + * @author Chuck Hagenbuch + * @category Horde + * @license http://www.horde.org/licenses/bsd BSD + * @package Log + * @subpackage Formatters + */ +class SimpleFormatter implements LogFormatter +{ + /** + * Format string. + * + * @var string + */ + protected $format; + + /** + * Constructor. + * + * @param string[] $options Configuration options: + *
+     * 'format' - (string) The log template.
+     * 
+ * + * @throws InvalidArgumentException + */ + public function __construct($options = null) + { + $format = (is_array($options) && isset($options['format'])) + ? $options['format'] + : $options; + + if (is_null($format)) { + $format = '%timestamp% %levelName%: %message%' . PHP_EOL; + } + + if (!is_string($format)) { + throw new InvalidArgumentException('Format must be a string'); + } + + $this->format = $format; + } + + /** + * Formats an event to be written by the handler. + * + * @param LogMessage $event Log event. + * + * @return string Formatted line. + */ + public function format(LogMessage $event): string + { + $output = $this->format; + $context = $event->context(); + $context['message'] = $event->formattedMessage(); + foreach ($context as $name => $value) { + $output = str_replace("%$name%", $value, $output); + } + return $output; + } +} diff --git a/src/Formatter/XmlFormatter.php b/src/Formatter/XmlFormatter.php new file mode 100644 index 0000000..9d00a80 --- /dev/null +++ b/src/Formatter/XmlFormatter.php @@ -0,0 +1,83 @@ + + * @author Chuck Hagenbuch + * @category Horde + * @license http://www.horde.org/licenses/bsd BSD + * @package Log + * @subpackage Formatters + */ +declare(strict_types=1); + +namespace Horde\Log\Formatter; + +use Horde\Log\LogFormatter; +use Horde\Log\LogMessage; +use DOMDocument; +use DOMElement; +use Horde\Log\LogException; + +/** + * @author Mike Naberezny + * @author Chuck Hagenbuch + * @category Horde + * @license http://www.horde.org/licenses/bsd BSD + * @package Log + * @subpackage Formatters + */ +class XmlFormatter implements LogFormatter +{ + /** + * Config options. + * + * @var string[] + */ + protected $options = [ + 'elementEntry' => 'log', + 'elementTimestamp' => 'timestamp', + 'elementMessage' => 'message', + 'elementLevel' => 'level', + 'lineEnding' => PHP_EOL, + ]; + + /** + * Constructor. + * + * @param string[] $options Config Options See $options property + */ + public function __construct(array $options = []) + { + $this->options = array_merge($this->options, $options); + } + + /** + * Formats an event to be written by the handler. + * + * @param LogMessage $event Log event. + * + * @return string XML string. + */ + public function format(LogMessage $event): string + { + $message = $event->formattedMessage(); + $level = $event->level()->name(); + $dom = new DOMDocument(); + + $elt = $dom->appendChild(new DOMElement($this->options['elementEntry'])); + $elt->appendChild(new DOMElement($this->options['elementTimestamp'], date('c'))); + $elt->appendChild(new DOMElement($this->options['elementMessage'], $message)); + $elt->appendChild(new DOMElement($this->options['elementLevel'], $level)); + + $xmlString = $dom->saveXML(); + if (!is_string($xmlString)) { + throw new LogException('Failed to dump XML string from DOM data'); + } + return preg_replace('/<\?xml version="1.0"( encoding="[^\"]*")?\?>\n/u', '', $xmlString) . $this->options['lineEnding']; + } +} diff --git a/src/Handler/BaseHandler.php b/src/Handler/BaseHandler.php new file mode 100644 index 0000000..4ed6ea7 --- /dev/null +++ b/src/Handler/BaseHandler.php @@ -0,0 +1,87 @@ + + * @author Chuck Hagenbuch + * @category Horde + * @license http://www.horde.org/licenses/bsd BSD + * @package Log + * @subpackage Handlers + */ +declare(strict_types=1); + +namespace Horde\Log\Handler; + +use Horde\Log\LogFilter; +use Horde\Log\LogHandler; +use Horde\Log\LogFormatter; +use Horde\Log\LogMessage; +use Horde\Log\LogException; + +/** + * @author Mike Naberezny + * @author Chuck Hagenbuch + * @category Horde + * @license http://www.horde.org/licenses/bsd BSD + * @package Log + * @subpackage Handlers + */ +abstract class BaseHandler implements LogHandler +{ + /** + * List of filters relevant only to this handler. + * + * @var LogFilter[] + */ + protected $filters = []; + + /** + * Formatters for this handler + * + * @var LogFormatter[] + */ + protected $formatters = []; + + /** + * Add a filter specific to this handler. + * + * Handlers cannot undo the filtering at logger level + * + * @param LogFilter $filter Filter to add. + */ + public function addFilter(LogFilter $filter): void + { + $this->filters[] = $filter; + } + + /** + * Log a message to this handler. + * + * Check all filters and expand it before delegating to the write method + * + * @param LogMessage $event Log event. + */ + public function log(LogMessage $event): void + { + // If any local filter rejects the message, don't log it. + foreach ($this->filters as $filter) { + if (!$filter->accept($event)) { + return; + } + } + $event->formatMessage($this->formatters); + $this->write($event); + } + + /** + * Buffer a message to be stored in the storage. + * + * @param LogMessage $event Log event. + */ + abstract public function write(LogMessage $event): bool; +} diff --git a/src/Handler/CliHandler.php b/src/Handler/CliHandler.php new file mode 100644 index 0000000..605b871 --- /dev/null +++ b/src/Handler/CliHandler.php @@ -0,0 +1,76 @@ + + * @category Horde + * @license http://www.horde.org/licenses/bsd BSD + * @package Log + * @subpackage Handlers + */ +declare(strict_types=1); + +namespace Horde\Log\Handler; + +use Horde\Log\LogFilter; +use Horde\Log\LogFormatter; +use Horde\Log\LogHandler; +use Horde\Log\LogMessage; +use Horde\Log\LogException; +use Horde\Log\Formatter\CliFormatter; +use Horde_Cli; + +/** + * Logs to the command line interface using Horde_Cli. + * + * @author Jan Schneider + * @category Horde + * @license http://www.horde.org/licenses/bsd BSD + * @package Log + * @subpackage Handlers + */ +class CliHandler extends StreamHandler +{ + use SetOptionsTrait; + /** + * A CLI handler. + * + * @var Horde_Cli + */ + protected $cli; + /** + * Options. + * + * @var Options + */ + protected Options $options; + + /** + * Class Constructor + * + * @param LogFormatter[]|null $formatters Log formatters. + * @param Horde_Cli|null $cli CLI Output object. + */ + public function __construct(array $formatters = null, Horde_Cli $cli = null, Options $options = null) + { + $this->options = $options ?? new Options(); + $this->cli = $cli ?? new Horde_Cli(); + $this->formatters = is_null($formatters) + ? [new CliFormatter($this->cli)] + : $formatters; + } + + /** + * Write a message to the log. + * + * @param LogMessage $event Log event. + * + * @return bool True. + * @throws LogException + */ + public function write($event): bool + { + $this->cli->writeln($event->formattedMessage()); + return true; + } +} diff --git a/src/Handler/FirebugHandler.php b/src/Handler/FirebugHandler.php new file mode 100644 index 0000000..2841a0c --- /dev/null +++ b/src/Handler/FirebugHandler.php @@ -0,0 +1,136 @@ + + * @author Chuck Hagenbuch + * @category Horde + * @license http://www.horde.org/licenses/bsd BSD + * @package Log + * @subpackage Handlers + */ +declare(strict_types=1); + +namespace Horde\Log\Handler; + +use Horde\Log\LogFilter; +use Horde\Log\LogFormatter; +use Horde\Log\LogHandler; +use Horde\Log\LogMessage; +use Horde\Log\LogException; +use Horde\Log\Formatter\SimpleFormatter; +use Horde_Log; + +/** + * @author Mike Naberezny + * @author Chuck Hagenbuch + * @category Horde + * @license http://www.horde.org/licenses/bsd BSD + * @package Log + * @subpackage Handlers + */ +class FirebugHandler extends BaseHandler +{ + use SetOptionsTrait; + private FirebugOptions $options; + /** + * Array of buffered output. + * + * @var array[] + */ + protected $buffer = []; + + /** + * Mapping of log priorities to Firebug methods. + * + * @var string[] + */ + protected static $methods = [ + Horde_Log::EMERG => 'error', + Horde_Log::ALERT => 'error', + Horde_Log::CRIT => 'error', + Horde_Log::ERR => 'error', + Horde_Log::WARN => 'warn', + Horde_Log::NOTICE => 'info', + Horde_Log::INFO => 'info', + Horde_Log::DEBUG => 'debug', + ]; + + /** + * Class Constructor + * + * @param LogFormatter[] $formatters Log formatter. + */ + public function __construct(FirebugOptions $options, array $formatters = null) + { + $this->options = $options ?? new FirebugOptions(); + $this->formatters = is_null($formatters) + ? [new SimpleFormatter()] + : $formatters; + } + + /** + * Write a message to the firebug console. This function really just + * writes the message to the buffer. If buffering is enabled, the + * message won't be output until the buffer is flushed. If + * buffering is not enabled, the buffer will be flushed + * immediately. + * + * @param LogMessage $event Log event. + * + * @return bool True. + */ + public function write(LogMessage $event): bool + { + $message = $event->formattedMessage(); + if (!empty($this->options->ident)) { + $message = $this->options->ident . ' ' . $message; + } + + $this->buffer[] = ['message' => $message, 'level' => $event->level()->criticality()]; + + if (empty($this->options->buffering)) { + $this->flush(); + } + + return true; + } + + /** + * Flush the buffer. + */ + public function flush(): bool + { + if (!count($this->buffer)) { + return true; + } + + $output = []; + foreach ($this->buffer as $event) { + $line = trim($event['message']); + + // Normalize line breaks. + $line = str_replace("\r\n", "\n", $line); + + // Escape line breaks + $line = str_replace("\n", "\\n\\\n", $line); + + // Escape quotes. + $line = str_replace('"', '\\"', $line); + + // Firebug call. + $method = self::$methods[$event['level']] + ?? 'log'; + $output[] = 'console.' . $method . '("' . $line . '");'; + } + + echo '\n"; + + $this->buffer = []; + return true; + } +} diff --git a/src/Handler/FirebugOptions.php b/src/Handler/FirebugOptions.php new file mode 100644 index 0000000..b7a3251 --- /dev/null +++ b/src/Handler/FirebugOptions.php @@ -0,0 +1,10 @@ + + * @category Horde + * @license http://www.horde.org/licenses/bsd BSD + * @package Log + * @subpackage Handlers + */ +declare(strict_types=1); + +namespace Horde\Log\Handler; + +use Horde\Log\LogFilter; +use Horde\Log\LogHandler; +use Horde\Log\LogFormatter; +use Horde\Log\LogMessage; +use Horde\Log\LogLevel; +use Horde\Log\LogException; +use InvalidArgumentException; +use Psr\Log\LoggerInterface; + +/** + * @author Ralf Lang + * @category Horde + * @license http://www.horde.org/licenses/bsd BSD + * @package Log + * @subpackage Handlers + */ +final class LoggerInterfaceHandler implements LogHandler +{ + private LoggerInterface $logger; + /** + * List of filters relevant only to this handler. + * + * @var LogFilter[] + */ + protected array $filters = []; + + /** + * Formatters for this handler. + * + * @var LogFormatter[] + */ + protected array $formatters = []; + + /** + * Constructor. + * + * @param LoggerInterface $logger The PSR-3 Logger to wrap + * @param LogFilter[] $filters Any filters to run before logging + * @param LogFormatter[] $formatters Any formatters to run before logging + */ + public function __construct(LoggerInterface $logger, array $filters = [], array $formatters = []) + { + $this->logger = $logger; + $this->filters = $filters; + $this->formatters = $formatters; + } + /** + * Add a filter specific to this handler. + * + * Handlers cannot undo the filtering at logger level + * + * @param LogFilter $filter Filter to add. + */ + public function addFilter(LogFilter $filter): void + { + $this->filters[] = $filter; + } + + /** + * Log a message to this handler. + * + * Check all filters and expand it before delegating to the write method + * + * @param LogMessage $event Log event. + */ + public function log(LogMessage $event): void + { + // If any local filter rejects the message, don't log it. + foreach ($this->filters as $filter) { + if (!$filter->accept($event)) { + return; + } + } + $event->formatMessage($this->formatters); + $this->write($event); + } + + /** + * Sets an option specific to the implementation of the log handler. + * + * @param string $optionKey Key name for the option to be changed. Keys + * are handler-specific. + * @param mixed $optionValue New value to assign to the option + * + * @return bool True. + * @throws LogException + */ + public function setOption($optionKey, $optionValue): bool + { + return true; + } + + /** + * Buffer a message to be stored in the storage. + * + * @param LogMessage $event Log event. + */ + public function write(LogMessage $event): bool + { + try { + $this->logger->log($event->level()->name(), $event->formattedMessage(), $event->context()); + } catch (InvalidArgumentException $e) { + return false; + } + return true; + } +} diff --git a/src/Handler/MockHandler.php b/src/Handler/MockHandler.php new file mode 100644 index 0000000..86a3491 --- /dev/null +++ b/src/Handler/MockHandler.php @@ -0,0 +1,74 @@ + + * @author Chuck Hagenbuch + * @category Horde + * @license http://www.horde.org/licenses/bsd BSD + * @package Log + * @subpackage Handlers + */ +declare(strict_types=1); + +namespace Horde\Log\Handler; + +use Horde\Log\LogFilter; +use Horde\Log\LogHandler; +use Horde\Log\LogMessage; +use Horde\Log\LogException; + +/** + * @author Mike Naberezny + * @author Chuck Hagenbuch + * @category Horde + * @license http://www.horde.org/licenses/bsd BSD + * @package Log + * @subpackage Handlers + */ +class MockHandler extends BaseHandler +{ + use SetOptionsTrait; + private Options $options; + + public function __construct(Options $options = null) + { + $this->options = $options ?? new Options(); + } + /** + * Log events. + * + * @var LogMessage[] + */ + public $events = []; + + /** + * Was shutdown called? + * + * @var bool + */ + public $shutdown = false; + + /** + * Write a message to the log. + * + * @param LogMessage $event Event data. + */ + public function write(LogMessage $event): bool + { + $this->events[] = $event; + return true; + } + + /** + * Record shutdown + */ + public function shutdown(): void + { + $this->shutdown = true; + } +} diff --git a/src/Handler/NullHandler.php b/src/Handler/NullHandler.php new file mode 100644 index 0000000..39c04ff --- /dev/null +++ b/src/Handler/NullHandler.php @@ -0,0 +1,52 @@ + + * @author Chuck Hagenbuch + * @category Horde + * @license http://www.horde.org/licenses/bsd BSD + * @package Log + * @subpackage Handlers + */ +declare(strict_types=1); + +namespace Horde\Log\Handler; + +use Horde\Log\Filter; +use Horde\Log\LogHandler; +use Horde\Log\LogMessage; +use Horde\Log\LogException; + +/** + * @author Mike Naberezny + * @author Chuck Hagenbuch + * @category Horde + * @license http://www.horde.org/licenses/bsd BSD + * @package Log + * @subpackage Handlers + */ +class NullHandler extends BaseHandler +{ + use SetOptionsTrait; + private Options $options; + + public function __construct(Options $options = null) + { + $this->options = $options ?? new Options(); + } + + /** + * Write a message to the log buffer. + * + * @return bool True. + */ + public function write(LogMessage $event): bool + { + return true; + } +} diff --git a/src/Handler/Options.php b/src/Handler/Options.php new file mode 100644 index 0000000..a1aa208 --- /dev/null +++ b/src/Handler/Options.php @@ -0,0 +1,10 @@ + + * @author Chuck Hagenbuch + * @category Horde + * @license http://www.horde.org/licenses/bsd BSD + * @package Log + * @subpackage Handlers + */ +declare(strict_types=1); + +namespace Horde\Log\Handler; + +use Horde\Log\Filter; +use Horde\Log\Formatter\SimpleFormatter; +use Horde\Log\LogHandler; +use Horde\Log\LogMessage; +use Horde\Log\LogException; +use Horde\Log\LogFormatter; +use Horde_Scribe_Client; + +/** + * @author Mike Naberezny + * @author Chuck Hagenbuch + * @category Horde + * @license http://www.horde.org/licenses/bsd BSD + * @package Log + * @subpackage Handlers + */ +class ScribeHandler extends BaseHandler +{ + use SetOptionsTrait; + private ScribeOptions $options; + /** + * Scribe client. + * + * @var Horde_Scribe_Client + */ + protected $scribe; + + /** + * Constructor. + * + * @param Horde_Scribe_Client $scribe Scribe client. + * @param LogFormatter[] $formatters Log formatter. + */ + public function __construct( + Horde_Scribe_Client $scribe, + array $formatters = null, + ScribeOptions $options = null + ) + { + $this->formatters = is_null($formatters) + ? [new SimpleFormatter()] + : $formatters; + $this->scribe = $scribe; + $this->options = $options ?? new ScribeOptions(); + } + + /** + * Write a message to the log. + * + * @param LogMessage $event Log event. + * + * @return bool True. + */ + public function write(LogMessage $event): bool + { + $context = $event->context(); + $message = $event->formattedMessage(); + + if (!empty($this->options->ident)) { + $message = $this->options->ident . ' ' . $message; + } + + $category = $context['category'] + ?? $this->options->category; + + if (!$this->options->addNewline) { + $message = rtrim($message); + } + $this->scribe->log($category, $message); + return true; + } +} diff --git a/src/Handler/ScribeOptions.php b/src/Handler/ScribeOptions.php new file mode 100644 index 0000000..f4d1a61 --- /dev/null +++ b/src/Handler/ScribeOptions.php @@ -0,0 +1,11 @@ +options->$optionKey)) { + throw new LogException('Unknown option "' . $optionKey . '".'); + } + $this->options->$optionKey = $optionValue; + + return true; + } +} diff --git a/src/Handler/StreamHandler.php b/src/Handler/StreamHandler.php new file mode 100644 index 0000000..80b7694 --- /dev/null +++ b/src/Handler/StreamHandler.php @@ -0,0 +1,135 @@ + + * @author Chuck Hagenbuch + * @category Horde + * @license http://www.horde.org/licenses/bsd BSD + * @package Log + * @subpackage Handlers + */ +declare(strict_types=1); + +namespace Horde\Log\Handler; + +use Horde\Log\Filter; +use Horde\Log\Formatter\SimpleFormatter; +use Horde\Log\LogFormatter; +use Horde\Log\LogHandler; +use Horde\Log\LogMessage; +use Horde\Log\LogException; + +/** + * @author Mike Naberezny + * @author Chuck Hagenbuch + * @category Horde + * @license http://www.horde.org/licenses/bsd BSD + * @package Log + * @subpackage Handlers + */ +class StreamHandler extends BaseHandler +{ + use SetOptionsTrait; + private Options $options; + + /** + * Holds the PHP stream to log to. + * + * @var null|resource + */ + protected $stream = null; + + /** + * The open mode. + * + * @var string + */ + protected string $mode; + + /** + * The stream uri to open. + * + * @var string + */ + protected $streamOrUrl; + + /** + * Class Constructor + * + * @param mixed $streamOrUrl Stream or URL to open as a + * stream. + * @param string $mode Mode, only applicable if a URL + * is given. + * @param LogFormatter[]|null $formatters Log formatter. + * + * @throws LogException + */ + public function __construct( + $streamOrUrl, + $mode = 'a+', + Options $options = null, + array $formatters = null + ) + { + $this->options = $options ?? new Options(); + $this->formatters = $formatters ?? [new SimpleFormatter()]; + $this->mode = $mode; + $this->streamOrUrl = $streamOrUrl; + + if (is_resource($streamOrUrl)) { + if (get_resource_type($streamOrUrl) != 'stream') { + throw new LogException(__CLASS__ . ': Resource is not a stream'); + } + + if ($mode && $mode != 'a+') { + throw new LogException(__CLASS__ . ': Mode cannot be changed on existing streams'); + } + + $this->stream = $streamOrUrl; + } else { + $this->__wakeup(); + } + } + + /** + * Wakup function - reattaches stream. + * + * @throws LogException + */ + public function __wakeup() + { + if (!($stream = @fopen($this->streamOrUrl, $this->mode, false))) { + throw new LogException(__CLASS__ . ': "' . $this->streamOrUrl . '" cannot be opened with mode "' . $this->mode . '"'); + } + $this->stream = $stream; + } + + /** + * Write a message to the log. + * + * @param LogMessage $event Log event. + * + * @return bool True. + * @throws LogException + */ + public function write(LogMessage $event): bool + { + $message = $event->formattedMessage(); + if (!empty($this->options->ident)) { + $message = $this->options->ident . ' ' . $message; + } + if (!is_resource($this->stream)) { + throw new LogException(__CLASS__ . ': Unable to write, no stream opened'); + } + if (!@fwrite($this->stream, $message)) { + throw new LogException(__CLASS__ . ': Unable to write to stream'); + } + + return true; + } +} diff --git a/src/Handler/SyslogHandler.php b/src/Handler/SyslogHandler.php new file mode 100644 index 0000000..8541d3d --- /dev/null +++ b/src/Handler/SyslogHandler.php @@ -0,0 +1,97 @@ + + * @author Chuck Hagenbuch + * @category Horde + * @license http://www.horde.org/licenses/bsd BSD + * @package Log + * @subpackage Handlers + */ +declare(strict_types=1); + +namespace Horde\Log\Handler; + +use Horde\Log\Filter; +use Horde\Log\LogHandler; +use Horde\Log\LogMessage; +use Horde\Log\LogException; + +/** + * @author Mike Naberezny + * @author Chuck Hagenbuch + * @category Horde + * @license http://www.horde.org/licenses/bsd BSD + * @package Log + * @subpackage Handlers + */ +class SyslogHandler extends BaseHandler +{ + use SetOptionsTrait; + /** + * Options to be set by setOption(). + * Sets openlog and syslog options. + * + * @var SyslogOptions + */ + protected SyslogOptions $options; + + /** + * Last ident set by a syslog-handler instance. + * + * @var string + */ + protected $lastIdent; + + /** + * Last facility name set by a syslog-handler instance. + * + * @var int + */ + protected int $lastFacility; + + public function __construct(SyslogOptions $options = null, array $formatters = [], array $filters = []) + { + $this->options = $options ?? new SyslogOptions(); + $this->formatters = $formatters; + $this->filters = $filters; + } + + /** + * Write a message to the log. + * + * @param LogMessage $event Log event. + * + * @return bool True. + * @throws LogException + */ + public function write(LogMessage $event): bool + { + if (($this->options->ident !== $this->lastIdent) || + ($this->options->facility !== $this->lastFacility)) { + $this->initializeSyslog(); + } + + $priority = $event->level()->criticality(); + if (!syslog($priority, $event->formattedMessage())) { + throw new LogException('Unable to log message'); + } + return true; + } + + /** + * Initialize syslog / set ident and facility. + * + * @throws LogException + */ + protected function initializeSyslog(): void + { + $this->lastIdent = $this->options->ident; + $this->lastFacility = $this->options->facility; + + if (!openlog($this->options->ident, $this->options->openLogOptions, $this->options->facility)) { + throw new LogException('Unable to open syslog'); + } + } +} diff --git a/src/Handler/SyslogOptions.php b/src/Handler/SyslogOptions.php new file mode 100644 index 0000000..9efc1d7 --- /dev/null +++ b/src/Handler/SyslogOptions.php @@ -0,0 +1,10 @@ + + * @author Chuck Hagenbuch + * @category Horde + * @license http://www.horde.org/licenses/bsd BSD + * @package Log + */ +declare(strict_types=1); + +namespace Horde\Log; + +use Horde\Exception\Wrapped; + +/** + * @author Mike Naberezny + * @author Chuck Hagenbuch + * @category Horde + * @license http://www.horde.org/licenses/bsd BSD + * @package Log + */ +class LogException extends Wrapped +{ +} diff --git a/src/LogFilter.php b/src/LogFilter.php new file mode 100644 index 0000000..dccaf1e --- /dev/null +++ b/src/LogFilter.php @@ -0,0 +1,50 @@ + + * @author Chuck Hagenbuch + * @category Horde + * @license http://www.horde.org/licenses/bsd BSD + * @package Log + * @subpackage Filters + */ +declare(strict_types=1); + +namespace Horde\Log; + +/** + * @category Horde + * @subpackage Filters + * @author Mike Naberezny + * @author Chuck Hagenbuch + * @license http://www.horde.org/licenses/bsd BSD + * @package Log + * @subpackage Filters + */ +interface LogFilter +{ + /** + * Accept a message. + */ + public const ACCEPT = true; + + /** + * Filter out a message. + */ + public const IGNORE = false; + + /** + * Returns Horde_Log_Filter::ACCEPT to accept the message, + * Horde_Log_Filter::IGNORE to ignore it. + * + * @param LogMessage $event Log event. + * + * @return bool Accepted? + */ + public function accept(LogMessage $event): bool; +} diff --git a/src/LogFormatter.php b/src/LogFormatter.php new file mode 100644 index 0000000..2b24c7f --- /dev/null +++ b/src/LogFormatter.php @@ -0,0 +1,36 @@ + + * @author Chuck Hagenbuch + * @category Horde + * @license http://www.horde.org/licenses/bsd BSD + * @package Log + */ +declare(strict_types=1); + +namespace Horde\Log; + +/** + * @author Mike Naberezny + * @author Chuck Hagenbuch + * @category Horde + * @license http://www.horde.org/licenses/bsd BSD + * @package Log + */ +interface LogFormatter +{ + /** + * Formats an event to be written by the handler. + * + * @param LogMessage $event Log event. + * + * @return string Formatted line. + */ + public function format(LogMessage $event): string; +} diff --git a/src/LogHandler.php b/src/LogHandler.php new file mode 100644 index 0000000..4b47d43 --- /dev/null +++ b/src/LogHandler.php @@ -0,0 +1,66 @@ + + * @license http://www.horde.org/licenses/bsd BSD + */ +declare(strict_types=1); + +namespace Horde\Log; + +use Horde\Util\HordeString; + +/** + * interface of a Log Handler. + * + * @category Horde + * @package Log + * @author Ralf Lang + * @license http://www.horde.org/licenses/bsd BSD + */ +interface LogHandler +{ + /** + * Add a filter specific to this handler. + * + * Handlers cannot undo the filtering at logger level + * + * @param LogFilter $filter Filter to add. + */ + public function addFilter(LogFilter $filter): void; + + /** + * Log a message to this handler. + * + * Check all filters and expand it before delegating to the write method + * + * @param LogMessage $event Log event. + */ + public function log(LogMessage $event): void; + + /** + * Sets an option specific to the implementation of the log handler. + * + * @param string $optionKey Key name for the option to be changed. Keys + * are handler-specific. + * @param mixed $optionValue New value to assign to the option + * + * @return bool True. + * @throws LogException + */ + public function setOption($optionKey, $optionValue): bool; + + /** + * Buffer a message to be stored in the storage. + * + * @param LogMessage $event Log event. + */ + public function write(LogMessage $event): bool; +} diff --git a/src/LogLevel.php b/src/LogLevel.php new file mode 100644 index 0000000..cac91d8 --- /dev/null +++ b/src/LogLevel.php @@ -0,0 +1,49 @@ + + * @license http://www.horde.org/licenses/bsd BSD + */ +declare(strict_types=1); + +namespace Horde\Log; + +use Horde\Util\HordeString; +use Psr\Log\LogLevel as PsrLogLevel; + +/** + * Represents a single log level. + * + * @category Horde + * @package Log + * @author Ralf Lang + * @license http://www.horde.org/licenses/bsd BSD + */ +class LogLevel extends PsrLogLevel +{ + private int $criticality; + private string $name; + + public function __construct(int $criticality, string $name) + { + $this->criticality = $criticality; + $this->name = HordeString::lower($name); + } + + public function criticality(): int + { + return $this->criticality; + } + + public function name(): string + { + return $this->name; + } +} diff --git a/src/LogLevels.php b/src/LogLevels.php new file mode 100644 index 0000000..df226d1 --- /dev/null +++ b/src/LogLevels.php @@ -0,0 +1,136 @@ + + * @license http://www.horde.org/licenses/bsd BSD + */ +declare(strict_types=1); + +namespace Horde\Log; + +use Horde\Util\HordeString; +use Psr\Log\InvalidArgumentException; + +/** + * A list of log levels to be recognized. + * + * @category Horde + * @package Log + * @author Ralf Lang + * @license http://www.horde.org/licenses/bsd BSD + */ +class LogLevels +{ + /** + * The configured levels. + * + * @var LogLevel[] + */ + private array $levels = []; + + public function register(LogLevel $level): void + { + /** + * TODO: Sanity checks, prevent registering the same name twice. + */ + $this->levels[] = $level; + } + + public static function initWithCanonicalLevels(): self + { + return new self( + [ + new LogLevel(0, 'emergency'), + new LogLevel(1, 'alert'), + new LogLevel(2, 'critical'), + new LogLevel(3, 'error'), + new LogLevel(4, 'warning'), + new LogLevel(5, 'notice'), + new LogLevel(6, 'info'), + new LogLevel(7, 'debug'), + ] + ); + } + + /** + * Register the canonical log levels and their popular aliases. + */ + public static function initWithAliasLevels(): self + { + return new self( + [ + new LogLevel(0, 'emergency'), + new LogLevel(0, 'emerg'), + new LogLevel(1, 'alert'), + new LogLevel(2, 'critical'), + new LogLevel(2, 'crit'), + new LogLevel(3, 'error'), + new LogLevel(3, 'err'), + new LogLevel(4, 'warning'), + new LogLevel(4, 'warn'), + new LogLevel(5, 'notice'), + new LogLevel(6, 'info'), + new LogLevel(6, 'information'), + new LogLevel(6, 'informational'), + new LogLevel(7, 'debug'), + ] + ); + } + + /** + * Get a registered log level by criticality. + * + * First match wins. No match throws exception. + * + * @param int $criticality + * @return LogLevel + * @throws InvalidArgumentException + */ + public function getByCriticality(int $criticality): LogLevel + { + foreach ($this->levels as $level) { + if ($criticality === $level->criticality()) { + return $level; + } + } + throw new InvalidArgumentException('Tried to get unregistered LogLevel by criticality: ' . $criticality); + } + + /** + * Get a registered log level by name or throw an exception. + * + * Matching is case insensitive. + * No Match throws exception; + * + * @param string $name + * @return LogLevel + * @throws InvalidArgumentException + */ + public function getByLevelName(string $name): LogLevel + { + foreach ($this->levels as $level) { + if (HordeString::lower($name) == $level->name()) { + return $level; + } + } + throw new InvalidArgumentException('Tried to get unregistered LogLevel by name: ' . $name); + } + + /** + * Constructor. + * + * @param LogLevel[] $levels Setup with these levels. + * Circumvents any sanity checks of register + */ + public function __construct(array $levels = []) + { + $this->levels = $levels; + } +} diff --git a/src/LogMessage.php b/src/LogMessage.php new file mode 100644 index 0000000..8201b8b --- /dev/null +++ b/src/LogMessage.php @@ -0,0 +1,126 @@ + + * @license http://www.horde.org/licenses/bsd BSD + */ +declare(strict_types=1); + +namespace Horde\Log; + +use Horde\Util\HordeString; +use Stringable; + +/** + * Represents a single log message. + * + * @category Horde + * @package Log + * @author Ralf Lang + * @license http://www.horde.org/licenses/bsd BSD + */ +class LogMessage implements Stringable +{ + private string $message; + private LogLevel $level; + /** + * Context may be a hash of anything, but only primitives and Stringables are expanded. + * + * @var mixed[] + */ + private array $context; + private string $formattedMessage; + + /** + * Constructor. + * + * @param LogLevel $level + * @param string $message + * @param mixed[] $context + */ + public function __construct(LogLevel $level, string $message, array $context = []) + { + $this->message = $message; + $this->level = $level; + $this->context = $context; + // We cannot safely assume timestamp is any specific format + if (!isset($this->context['timestamp'])) { + $this->context['timestamp'] = time(); + } + } + + /** + * Merge an additional context with the current context. + * + * On existing key, last write wins. + * + * @param mixed[] $context + * @return void + */ + public function mergeContext(array $context): void + { + $this->context = array_merge($this->context, $context); + } + + /** + * Expose context. + * + * @return mixed[] + */ + public function context(): array + { + return $this->context; + } + + public function message(): string + { + return $this->message; + } + + /** + * The formatted message. + * + * As each handler may have its own formatters, calling into this method + * should be left to handlers and formatters. + * + * @internal The (preliminary) formatting result + * + * @return string + */ + public function formattedMessage(): string + { + return $this->formattedMessage; + } + + /** + * Apply formatters to the message;. + * + * @param LogFormatter[] $formatters + * @return string + */ + public function formatMessage(array $formatters): string + { + $this->formattedMessage = $this->message; + foreach ($formatters as $formatter) { + $this->formattedMessage = $formatter->format($this); + } + return $this->formattedMessage; + } + + public function level(): LogLevel + { + return $this->level; + } + + public function __toString(): string + { + return $this->formattedMessage(); + } +} diff --git a/src/Logger.php b/src/Logger.php new file mode 100644 index 0000000..fcd308e --- /dev/null +++ b/src/Logger.php @@ -0,0 +1,324 @@ + + * @author Chuck Hagenbuch + * @category Horde + * @license http://www.horde.org/licenses/bsd BSD + * @package Log + */ +declare(strict_types=1); + +namespace Horde\Log; + +use Horde\Util\HordeString; +use Psr\Log\InvalidArgumentException; +use Stringable; + +/** + * @author Mike Naberezny + * @author Chuck Hagenbuch + * @author Ralf Lang + * @category Horde + * @license http://www.horde.org/licenses/bsd BSD + * @package Log + */ +class Logger implements LoggerInterface +{ + /* Serialize version. */ + public const VERSION = 1; + + /** + * Log levels where the keys are the level priorities and the values are + * the level names. + * + * @var LogLevels + */ + protected LogLevels $levels; + + /** + * Handler objects. + * + * @var LogHandler[] + */ + protected $handlers = []; + + /** + * Horde_Log_Filter objects. + * + * @var LogFilter[] + */ + protected $filters = []; + + /** + * Constructor. + * + * @param LogHandler[] $handlers The list of handlers. + * TODO: Is defaulting to the null handler any better than defaulting to no handler? + * @param LogLevels|null $levels A list of log levels to operate on. Null initializes with the RFC loglevels + * @param LogFilter[] $filters A list of global filters to apply before any log handler. Log handlers may have their own filters + */ + public function __construct(array $handlers = [], LogLevels $levels = null, array $filters = []) + { + if ($levels) { + $this->levels = $levels; + } else { + $levels = LogLevels::initWithCanonicalLevels(); + } + foreach ($handlers as $handler) { + $this->addHandler($handler); + } + foreach ($filters as $filter) { + $this->addFilter($filter); + } + } + + /** + * Serialize. + * + * @return string Serialized representation of this object. + */ + public function serialize() + { + return serialize([ + self::VERSION, + $this->filters, + $this->handlers, + ]); + } + + /** + * Unserialize. + * + * @param string $data Serialized data. + * + * @throws LogException + */ + public function unserialize($data): void + { + $data = @unserialize($data); + if (!is_array($data) || + !isset($data[0]) || + ($data[0] != self::VERSION)) { + throw new LogException('Cache version change'); + } + + $this->filters = $data[1]; + $this->handlers = $data[2]; + } + + /** + * Undefined method handler allows a shortcut: + *
+     * $log->levelName('message');
+     *   instead of
+     * $log->log('message', Horde_Log_LEVELNAME);
+     * 
. + * + * @param string $method Log level name. + * @param string|object|stringable $params Message to log. + * @param array $context The context for the message. + */ + /* public function __call($method, $params) + { + TODO: Do we really want to support that mess? + We already support the canonic names + }*/ + + /* + The Logger methods. + Compared to PSR-3 type hints, we explicitly hint against LogMessage to + provide specific behavior. This is why we copy it rather than using the trait + */ + /** + * System is unusable. + * + * @param string|Stringable|LogMessage $message + * @param mixed[] $context + * + * @return void + */ + public function emergency($message, array $context = []): void + { + $this->log(LogLevel::EMERGENCY, $message, $context); + } + + /** + * Action must be taken immediately. + * + * Example: Entire website down, database unavailable, etc. This should + * trigger the SMS alerts and wake you up. + * + * @param string|Stringable|LogMessage $message + * @param mixed[] $context + * + * @return void + */ + public function alert($message, array $context = []): void + { + $this->log(LogLevel::ALERT, $message, $context); + } + + /** + * Critical conditions. + * + * Example: Application component unavailable, unexpected exception. + * + * @param string|Stringable|LogMessage $message + * @param mixed[] $context + * + * @return void + */ + public function critical($message, array $context = []): void + { + $this->log(LogLevel::CRITICAL, $message, $context); + } + + /** + * Runtime errors that do not require immediate action but should typically + * be logged and monitored. + * + * @param string|Stringable|LogMessage $message The message to submit + * @param mixed[] $context An array of context + * + * @return void + */ + public function error($message, array $context = []): void + { + $this->log(LogLevel::ERROR, $message, $context); + } + + /** + * Exceptional occurrences that are not errors. + * + * Example: Use of deprecated APIs, poor use of an API, undesirable things + * that are not necessarily wrong. + * + * @param string|Stringable|LogMessage $message + * @param mixed[] $context + * + * @return void + */ + public function warning($message, array $context = []): void + { + $this->log(LogLevel::WARNING, $message, $context); + } + + /** + * Normal but significant events. + * + * @param string|Stringable|LogMessage $message + * @param mixed[] $context + * + * @return void + */ + public function notice($message, array $context = []): void + { + $this->log(LogLevel::NOTICE, $message, $context); + } + + /** + * Interesting events. + * + * Example: User logs in, SQL logs. + * + * @param string|Stringable|LogMessage $message + * @param mixed[] $context + * + * @return void + */ + public function info($message, array $context = []): void + { + $this->log(LogLevel::INFO, $message, $context); + } + + /** + * Detailed debug information. + * + * @param string $message + * @param mixed[] $context + * + * @return void + */ + public function debug($message, array $context = []): void + { + $this->log(LogLevel::DEBUG, $message, $context); + } + /** + * Logs with an arbitrary level. + * + * @param int|string|LogLevel $level + * @param string|Stringable|LogMessage $message + * @param mixed[] $context User code supplied additional info + * + * @return void + * + * @throws \Psr\Log\InvalidArgumentException + */ + public function log($level, $message, array $context = []): void + { + $loglevel = null; + // Error if the requested level is not present + if ($level instanceof LogLevel) { + $this->levels->getByCriticality($level->criticality()); + $this->levels->getByLevelName($level->name()); + $loglevel = $level; + } elseif (is_int($level)) { + $loglevel = $this->levels->getByCriticality($level); + } elseif (is_string($level)) { + $loglevel = $this->levels->getByLevelName($level); + } + if (is_null($loglevel)) { + throw new InvalidArgumentException('Unsupported log level type, try string or numeric'); + } + // Special handling of submitted native LogMessage objects + if ($message instanceof LogMessage) { + $logMessage = new LogMessage($loglevel, $message->message(), $message->context()); + $logMessage->mergeContext($context); + } else { + // The log message will auto-generate context[timestamp] unless missing + $logMessage = new LogMessage($loglevel, (string) $message, $context); + } + + // Apply any global prefilters, may reject the message + foreach ($this->filters as $filter) { + if (!$filter->accept($logMessage)) { + return; + } + } + + // Delegate to all registered handlers + foreach ($this->handlers as $handler) { + // Any processing and interpolation is up to the log handler + $handler->log($logMessage); + } + } + + /** + * Add a filter that will be applied before all log handlers. + * Before a message will be received by any of the handlers, it + * must be accepted by all filters added with this method. + * + * @param LogFilter $filter Filter to add. + */ + public function addFilter(LogFilter $filter): void + { + $this->filters[] = $filter; + } + + /** + * Add a handler. A handler is responsible for taking a log + * message and writing it out to storage. + * + * @param LogHandler $handler Handler to add. + */ + public function addHandler(LogHandler $handler): void + { + $this->handlers[] = $handler; + } +} diff --git a/src/LoggerBuilder.php b/src/LoggerBuilder.php new file mode 100644 index 0000000..1fd4541 --- /dev/null +++ b/src/LoggerBuilder.php @@ -0,0 +1,88 @@ + + * @category Horde + * @license http://www.horde.org/licenses/bsd BSD + * @package Log + */ +class LoggerBuilder +{ + private LogLevels $loglevels; + private Logger $logger; + + public function __construct(LogLevels $loglevels = null) + { + $this->reset($loglevels); + } + /** + * Create an all-new logger instance without any handler or filter + * + */ + public function reset(LogLevels $loglevels = null): self + { + $this->loglevels = $loglevels ?? LogLevels::initWithCanonicalLevels(); + $this->logger = new Logger([], $loglevels); + return $this; + } + + /** + * Create a custom log level for the current logger + * + * @param integer $criticality + * @param string $name + * @return self + */ + public function withLogLevel(int $criticality, string $name): self + { + $level = new LogLevel($criticality, $name); + // The logger and the builder share the reference to the same object + // No need for an actual injection into the logger + $this->loglevels->register($level); + return $this; + } + + /** + * Add a log handler to a logger + * + * @param LogHandler $handler + * @return self + */ + public function withLogHandler(LogHandler $handler): self + { + $this->logger->addHandler($handler); + return $this; + } + + /** + * Return a logger + * + * Resets the builder to default state + * + * @return Logger + */ + public function build(): Logger + { + $logger = $this->logger; + $this->reset(); + return $logger; + } + + /** + * Add a filter + * + * @param LogFilter $filter + * @return self + */ + public function withGlobalFilter(LogFilter $filter): self + { + return $this; + } + +} \ No newline at end of file diff --git a/src/LoggerInterface.php b/src/LoggerInterface.php new file mode 100644 index 0000000..acbf869 --- /dev/null +++ b/src/LoggerInterface.php @@ -0,0 +1,40 @@ + + */ +interface LoggerInterface extends PsrLoggerInterface +{ + /** + * Add a filter that will be applied before all log handlers. + * + * Before a message will be received by any of the handlers, it + * must be accepted by all filters added with this method. + * + * @param LogFilter $filter Filter to add. + */ + public function addFilter(LogFilter $filter): void; +} diff --git a/test/AllTests.php b/test/AllTests.php new file mode 100644 index 0000000..1f7a9bf --- /dev/null +++ b/test/AllTests.php @@ -0,0 +1,5 @@ +run(); diff --git a/test/Horde/Log/Filter/ChainingTest.php b/test/Filter/ChainingTest.php similarity index 75% rename from test/Horde/Log/Filter/ChainingTest.php rename to test/Filter/ChainingTest.php index 9ac92d2..f4c0a0a 100644 --- a/test/Horde/Log/Filter/ChainingTest.php +++ b/test/Filter/ChainingTest.php @@ -13,6 +13,11 @@ * @package Log * @subpackage UnitTests */ +namespace Horde\Log\Test\Filter; +use \PHPUnit\Framework\TestCase; +use \Horde_Log; +use \Horde_Log_Logger; +use \Horde_Log_Handler_Stream; /** * @author Mike Naberezny @@ -22,9 +27,9 @@ * @package Log * @subpackage UnitTests */ -class Horde_Log_Filter_ChainingTest extends PHPUnit_Framework_TestCase +class ChainingTest extends TestCase { - public function setUp() + public function setUp(): void { date_default_timezone_set('America/New_York'); @@ -33,7 +38,7 @@ public function setUp() $this->logger->addHandler(new Horde_Log_Handler_Stream($this->log)); } - public function tearDown() + public function tearDown(): void { fclose($this->log); } @@ -49,8 +54,8 @@ public function testFilterAllHandlers() rewind($this->log); $logdata = stream_get_contents($this->log); - $this->assertNotContains($ignored, $logdata); - $this->assertContains($logged, $logdata); + $this->assertStringNotContainsString($ignored, $logdata); + $this->assertStringContainsString($logged, $logdata); } @@ -67,13 +72,13 @@ public function testFilterOnSpecificHandler() rewind($this->log); $logdata = stream_get_contents($this->log); - $this->assertContains($warn, $logdata); - $this->assertContains($err, $logdata); + $this->assertStringContainsString($warn, $logdata); + $this->assertStringContainsString($err, $logdata); rewind($log2); $logdata = stream_get_contents($log2); - $this->assertContains($err, $logdata); - $this->assertNotContains($warn, $logdata); + $this->assertStringContainsString($err, $logdata); + $this->assertStringNotContainsString($warn, $logdata); } } diff --git a/test/Filter/ConstraintFilterTest.php b/test/Filter/ConstraintFilterTest.php new file mode 100644 index 0000000..b691def --- /dev/null +++ b/test/Filter/ConstraintFilterTest.php @@ -0,0 +1,129 @@ + + * @category Horde + * @license http://www.horde.org/licenses/bsd BSD + * @package Log + * @subpackage UnitTests + */ + +namespace Horde\Log\Test\Filter; + +use PHPUnit\Framework\TestCase; + +use Horde\Log\Filter\ConstraintFilter; +use Horde\Log\LogMessage; +use Horde\Log\LogLevel; +use Horde_Constraint_AlwaysFalse; + +class ConstraintFilterTest extends TestCase +{ + public function setUp(): void + { + $this->level1 = new LogLevel(1, 'testName1'); + $this->level2 = new LogLevel(2, 'testName2'); + $this->level3 = new LogLevel(3, 'testName3'); + $this->level4 = new LogLevel(4, 'testName4'); + $this->message1 = 'testMessage1'; + $this->message2 = 'required_field'; + $this->message3 = 'somevalue'; + $this->message4 = 'multiple required fields'; + $this->logMessage1 = new LogMessage($this->level1, $this->message1); + $this->logMessage2 = new LogMessage($this->level2, $this->message2); + $this->logMessage3 = new LogMessage($this->level3, $this->message3, ['customField3' => 'custumValue3']); + $this->logMessage4 = new LogMessage($this->level4, $this->message4, ['customField4' => 'custumValue4', 'customField5' => 'custumValue5']); + } + + public function testFilterAcceptMultipleRequiredFieldsIfPresent() + { + $filterator = new ConstraintFilter(); + $filterator->addRequiredFields('customField4', 'customField5'); + $this->assertTrue($filterator->accept($this->logMessage4)); + } + + public function testFilterNotAcceptMultipleRequiredFieldsIfNotPresent() + { + $filterator = new ConstraintFilter(); + $filterator->addRequiredFields('customField3', 'customField4'); + $this->assertFalse($filterator->accept($this->logMessage4)); + } + + + public function testFilterDoesNotAcceptWhenRequiredFieldIsMissing() + { + $filterator = new ConstraintFilter(); + $filterator->addRequiredField('required_field'); + $this->assertFalse($filterator->accept($this->logMessage3)); + } + + + public function testFilterAcceptsWhenRequiredFieldisPresent() + { + $filterator = new ConstraintFilter(); + $filterator->addRequiredField('customField3'); + $this->assertTrue($filterator->accept($this->logMessage3)); + } + + public function testFilterAcceptsWhenRegexMatchesField() + { + $filterator = new ConstraintFilter(); + $filterator->addRegex('customField3', '/cust*/'); + + $this->assertTrue($filterator->accept($this->logMessage3)); + } + + public function testFilterAcceptsWhenRegex_DOESNOT_MatcheField() + { + $filterator = new ConstraintFilter(); + $filterator->addRegex('customField3', '/this value does not exist/'); + + $this->assertFalse($filterator->accept($this->logMessage3)); + } + + private function getConstraintMock($returnVal) + { + $const = $this->getMockBuilder('Horde_Constraint', array('evaluate'))->getMock(); + $const->expects($this->once()) + ->method('evaluate') + ->will($this->returnValue($returnVal)); + return $const; + } + + public function testFilterCallsEvalOnAllConstraintsWhenTheyAreAllTrue() + { + $filterator = new ConstraintFilter(); + $filterator->addConstraint('context', $this->getConstraintMock(true)); + $filterator->addConstraint('level', $this->getConstraintMock(true)); + $filterator->addConstraint('message', $this->getConstraintMock(true)); + $filterator->addConstraint('dafdfadg234435dafdf', $this->getConstraintMock(true)); + + $filterator->accept($this->logMessage3); + } + + public function testFilterStopsWhenItFindsAFalseCondition() + { + $filterator = new ConstraintFilter(); + $filterator->addConstraint('fieldname', $this->getConstraintMock(true)); + $filterator->addConstraint('fieldname', $this->getConstraintMock(true)); + $filterator->addConstraint('fieldname', new Horde_Constraint_AlwaysFalse()); + + $const = $this->getMockBuilder('Horde_Constraint', array('evaluate'))->getMock(); + $const->expects($this->never()) + ->method('evaluate'); + $filterator->addConstraint('fieldname', $const); + $filterator->accept($this->logMessage3); + } + + public function testFilterAcceptCallsConstraintOnNullWhenFieldDoesnotExist() + { + $filterator = new ConstraintFilter(); + $const = $this->getMockBuilder('Horde_Constraint', array('evaluate'))->getMock(); + $const->expects($this->once()) + ->method('evaluate') + ->with(null); + $filterator->addConstraint('non existant field', $const); + $filterator->accept($this->logMessage2); + } +} diff --git a/test/Horde/Log/Filter/ConstraintTest.php b/test/Filter/ConstraintTest.php similarity index 88% rename from test/Horde/Log/Filter/ConstraintTest.php rename to test/Filter/ConstraintTest.php index 19c0e9f..b0ad248 100644 --- a/test/Horde/Log/Filter/ConstraintTest.php +++ b/test/Filter/ConstraintTest.php @@ -8,6 +8,10 @@ * @package Log * @subpackage UnitTests */ +namespace Horde\Log\Test\Filter; +Use \Horde_Test_Case; +use \Horde_Log_Filter_Constraint; +use \Horde_Constraint_AlwaysFalse; /** * @author James Pepin @@ -16,7 +20,7 @@ * @package Log * @subpackage UnitTests */ -class Horde_Log_Filter_ConstraintTest extends Horde_Test_Case +class ConstraintTest extends Horde_Test_Case { public function testFilterDoesNotAcceptWhenRequiredFieldIsMissing() { @@ -67,7 +71,7 @@ public function testFilterAcceptsWhenRegex_DOESNOT_MatcheField() private function getConstraintMock($returnVal) { - $const = $this->getMock('Horde_Constraint', array('evaluate')); + $const = $this->getMockBuilder('Horde_Constraint', array('evaluate'))->getMock(); $const->expects($this->once()) ->method('evaluate') ->will($this->returnValue($returnVal)); @@ -90,7 +94,7 @@ public function testFilterStopsWhenItFindsAFalseCondition() $filterator->addConstraint('fieldname', $this->getConstraintMock(true)); $filterator->addConstraint('fieldname', new Horde_Constraint_AlwaysFalse()); - $const = $this->getMock('Horde_Constraint', array('evaluate')); + $const = $this->getMockBuilder('Horde_Constraint', array('evaluate'))->getMock(); $const->expects($this->never()) ->method('evaluate'); $filterator->addConstraint('fieldname', $const); @@ -101,7 +105,7 @@ public function testFilterStopsWhenItFindsAFalseCondition() public function testFilterAcceptCallsConstraintOnNullWhenFieldDoesnotExist() { $filterator = new Horde_Log_Filter_Constraint(); - $const = $this->getMock('Horde_Constraint', array('evaluate')); + $const = $this->getMockBuilder('Horde_Constraint', array('evaluate'))->getMock(); $const->expects($this->once()) ->method('evaluate') ->with(null); diff --git a/test/Filter/ExactLevelFilterTest.php b/test/Filter/ExactLevelFilterTest.php new file mode 100644 index 0000000..ba531b5 --- /dev/null +++ b/test/Filter/ExactLevelFilterTest.php @@ -0,0 +1,68 @@ + + * @author Chuck Hagenbuch + * @category Horde + * @license http://www.horde.org/licenses/bsd BSD + * @package Log + * @subpackage UnitTests + */ + +namespace Horde\Log\Test\Filter; +use PHPUnit\Framework\TestCase; +use Horde\Log\Filter\ExactLevelFilter; +use Horde\Log\LogFilter; +use Horde\Log\LogLevel; +use Horde\Log\LogMessage; +use TypeError; + +/** + * @author Mike Naberezny + * @author Chuck Hagenbuch + * @category Horde + * @license http://www.horde.org/licenses/bsd BSD + * @package Log + * @subpackage UnitTests + */ +class ExactLevelFilterTest extends TestCase +{ + public function setUp(): void + { + // accept at and only at level 2 + $this->messageLogLevel1 = new LogLevel(1, 'fatal'); + $this->messageLogLevel2 = new LogLevel(2, 'error'); + $this->messageLogLevel3 = new LogLevel(3, 'warn'); + $this->messageLvl3 = new LogMessage($this->messageLogLevel3, 'test'); + $this->messageLvl2 = new LogMessage($this->messageLogLevel2, 'error'); + $this->messageLvl1 = new LogMessage($this->messageLogLevel1, 'test'); + $this->filter = new ExactLevelFilter(2); + $this->filter2 = new ExactLevelFilter(2, 'error'); + $this->filter3 = new ExactLevelFilter(2, 'ERROR'); + } + + public function testLevelFilterAccept() + { + $this->assertTrue($this->filter->accept($this->messageLvl2)); + $this->assertTrue($this->filter2->accept($this->messageLvl2)); + } + + public function testLevelFilterReject() + { + $this->assertFalse($this->filter->accept($this->messageLvl3)); + $this->assertFalse($this->filter->accept($this->messageLvl1)); + $this->assertFalse($this->filter2->accept($this->messageLvl1)); + $this->assertFalse($this->filter3->accept($this->messageLvl2)); + } + + public function testConstructorThrowsOnInvalidLevel() + { + $this->expectException(TypeError::class); + new ExactLevelFilter('foo','bar'); + } +} diff --git a/test/Horde/Log/Filter/ExactLevelTest.php b/test/Filter/ExactLevelTest.php similarity index 76% rename from test/Horde/Log/Filter/ExactLevelTest.php rename to test/Filter/ExactLevelTest.php index 3f648bb..88d7328 100644 --- a/test/Horde/Log/Filter/ExactLevelTest.php +++ b/test/Filter/ExactLevelTest.php @@ -13,6 +13,10 @@ * @package Log * @subpackage UnitTests */ +namespace Horde\Log\Test\Filter; +use \PHPUnit\Framework\TestCase; +use \Horde_Log_Filter_Level; +use \Horde_Log_Filter_ExactLevel; /** * @author Mike Naberezny @@ -22,9 +26,9 @@ * @package Log * @subpackage UnitTests */ -class Horde_Log_Filter_ExactLevelTest extends PHPUnit_Framework_TestCase +class ExactLevelTest extends TestCase { - public function setUp() + public function setUp(): void { // accept at and only at level 2 $this->filter = new Horde_Log_Filter_ExactLevel(2); @@ -43,12 +47,7 @@ public function testLevelFilterReject() public function testConstructorThrowsOnInvalidLevel() { - try { - new Horde_Log_Filter_Level('foo'); - $this->fail(); - } catch (Exception $e) { - $this->assertInstanceOf('InvalidArgumentException', $e); - $this->assertRegExp('/must be an integer/i', $e->getMessage()); - } + $this->expectException('InvalidArgumentException'); + new Horde_Log_Filter_Level('foo'); } } diff --git a/test/Horde/Log/Filter/LevelTest.php b/test/Filter/LevelTest.php similarity index 76% rename from test/Horde/Log/Filter/LevelTest.php rename to test/Filter/LevelTest.php index c9c281e..78ddd63 100644 --- a/test/Horde/Log/Filter/LevelTest.php +++ b/test/Filter/LevelTest.php @@ -13,6 +13,9 @@ * @package Log * @subpackage UnitTests */ +namespace Horde\Log\Test\Filter; +use \PHPUnit\Framework\TestCase; +use \Horde_Log_Filter_Level; /** * @author Mike Naberezny @@ -22,9 +25,9 @@ * @package Log * @subpackage UnitTests */ -class Horde_Log_Filter_LevelTest extends PHPUnit_Framework_TestCase +class LevelTest extends TestCase { - public function setUp() + public function setUp(): void { // accept at or below level 2 $this->filter = new Horde_Log_Filter_Level(2); @@ -43,12 +46,7 @@ public function testLevelFilterReject() public function testConstructorThrowsOnInvalidLevel() { - try { - new Horde_Log_Filter_Level('foo'); - $this->fail(); - } catch (Exception $e) { - $this->assertInstanceOf('InvalidArgumentException', $e); - $this->assertRegExp('/must be an integer/i', $e->getMessage()); - } + $this->expectException('InvalidArgumentException'); + new Horde_Log_Filter_Level('foo'); } } diff --git a/test/Filter/MaximumLevelFilterTest.php b/test/Filter/MaximumLevelFilterTest.php new file mode 100644 index 0000000..0936351 --- /dev/null +++ b/test/Filter/MaximumLevelFilterTest.php @@ -0,0 +1,56 @@ + + * @author Chuck Hagenbuch + * @category Horde + * @license http://www.horde.org/licenses/bsd BSD + * @package Log + * @subpackage UnitTests + */ + +namespace Horde\Log\Test\Filter; + +use PHPUnit\Framework\TestCase; +use Horde\Log\Filter\MaximumLevelFilter; +use Horde\Log\LogMessage; +use Horde\Log\LogLevel; +use TypeError; + +class MaximumLevelFilterTest extends TestCase +{ + public function setUp(): void + { + $this->filter = new MaximumLevelFilter(2); + } + + public function testLevelFilterAccept() + { + $level1 = new LogLevel(1, 'testName1'); + $level2 = new LogLevel(2, 'testName2'); + $message1 = 'testMessage1'; + $message2 = 'testMessage2'; + $logMessage1 = new LogMessage($level1, $message1); + $logMessage2 = new LogMessage($level2, $message2); + $this->assertTrue($this->filter->accept($logMessage1)); + $this->assertTrue($this->filter->accept($logMessage2)); + } + + public function testLevelFilterReject() + { + $level = new LogLevel(5, 'testName2'); + $logMessage = new LogMessage($level, ""); + $this->assertFalse($this->filter->accept($logMessage)); + } + + public function testConstructorThrowsOnInvalidLevel() + { + $this->expectException(TypeError::class); + new MaximumLevelFilter('testName2'); + } +} diff --git a/test/Filter/MessageFilterTest.php b/test/Filter/MessageFilterTest.php new file mode 100644 index 0000000..e0c2821 --- /dev/null +++ b/test/Filter/MessageFilterTest.php @@ -0,0 +1,51 @@ + + * @category Horde + * @license http://www.horde.org/licenses/bsd BSD + * @package Log + * @subpackage UnitTests + */ +namespace Horde\Log\Test\Filter; + + +use \PHPUnit\Framework\TestCase; +use Horde\Log\Filter\MessageFilter; +use Horde\Log\LogMessage; +use Horde\Log\LogLevel; + + + + +class MessageFilterTest extends TestCase +{ + + public function setUp(): void + { + $this->level1 = new LogLevel(1, 'testName1'); + $this->level2 = new LogLevel(2, 'testName2'); + $this->message1 = "foo accept bar"; + $this->message2 = "foo reject bar"; + $this->logMessage1 = new LogMessage($this->level1, $this->message1); + $this->logMessage2 = new LogMessage($this->level2, $this->message2); + } + + public function testMessageFilterRecognizesInvalidRegularExpression(){ + $this->expectException('InvalidArgumentException'); + new MessageFilter('invalid regexp'); + } + + public function testMessageFilter() + { + $filter = new MessageFilter('/accept/'); + $this->assertTrue($filter->accept($this->logMessage1)); + $this->assertFalse($filter->accept($this->logMessage2)); + } + +} \ No newline at end of file diff --git a/test/Horde/Log/Filter/MessageTest.php b/test/Filter/MessageTest.php similarity index 77% rename from test/Horde/Log/Filter/MessageTest.php rename to test/Filter/MessageTest.php index 4bd8b9c..f07896d 100644 --- a/test/Horde/Log/Filter/MessageTest.php +++ b/test/Filter/MessageTest.php @@ -13,6 +13,9 @@ * @package Log * @subpackage UnitTests */ +namespace Horde\Log\Test\Filter; +use \PHPUnit\Framework\TestCase; +use \Horde_Log_Filter_Message; /** * @author Mike Naberezny @@ -22,17 +25,13 @@ * @package Log * @subpackage UnitTests */ -class Horde_Log_Filter_MessageTest extends PHPUnit_Framework_TestCase +class MessageTest extends TestCase { public function testMessageFilterRecognizesInvalidRegularExpression() { - try { - $filter = new Horde_Log_Filter_Message('invalid regexp'); - $this->fail(); - } catch (InvalidArgumentException $e) { - $this->assertRegexp('/invalid reg/i', $e->getMessage()); - } + $this->expectException('InvalidArgumentException'); + new Horde_Log_Filter_Message('invalid regexp'); } public function testMessageFilter() diff --git a/test/Filter/MinimumLevelFilterTest.php b/test/Filter/MinimumLevelFilterTest.php new file mode 100644 index 0000000..e4107b3 --- /dev/null +++ b/test/Filter/MinimumLevelFilterTest.php @@ -0,0 +1,56 @@ +filter = new MinimumLevelFilter(2); + } + + public function testLevelFilterAccept() + { + $level1 = new LogLevel(1, 'testName1'); + $level2 = new LogLevel(2, 'testName2'); + $message1 = 'testMessage1'; + $message2 = 'testMessage2'; + $logMessage1 = new LogMessage($level1, $message1); + $logMessage2 = new LogMessage($level2, $message2); + $this->assertFalse($this->filter->accept($logMessage1)); + $this->assertTrue($this->filter->accept($logMessage2)); + } + + public function testLevelFilterReject() + { + $level = new LogLevel(5, 'testName2'); + $logMessage = new LogMessage($level, ""); + $this->assertTrue($this->filter->accept($logMessage)); + } + + public function testConstructorThrowsOnInvalidLevel() + { + $this->expectException(TypeError::class); + new MinimumLevelFilter('testName2'); + } +} diff --git a/test/Filter/SuppressFilterTest.php b/test/Filter/SuppressFilterTest.php new file mode 100644 index 0000000..d8bb8b1 --- /dev/null +++ b/test/Filter/SuppressFilterTest.php @@ -0,0 +1,72 @@ + + * @category Horde + * @license http://www.horde.org/licenses/bsd BSD + * @package Log + * @subpackage UnitTests + */ +namespace Horde\Log\Test\Filter; + +use \PHPUnit\Framework\TestCase; +use Horde\Log\Filter\SuppressFilter; +use Horde\Log\LogMessage; +use Horde\Log\LogLevel; + + + + +class SuppressFilterTest extends TestCase +{ + public function setUp(): void + { + $this->filter = new SuppressFilter(); + $this->level1 = new LogLevel(1, 'testName1'); + $this->level2 = new LogLevel(2, 'testName2'); + $this->level3 = new LogLevel(3, 'testName3'); + $this->message1 = "test1"; + $this->message2 = "test2"; + $this->message3 = "test3"; + $this->logMessage1 = new LogMessage($this->level1, $this->message1); + $this->logMessage2 = new LogMessage($this->level2, $this->message2); + $this->logMessage3 = new LogMessage($this->level3, $this->message3); + } + + + public function testSuppressIsInitiallyOff() + { + $this->assertTrue($this->filter->accept($this->logMessage1)); + } + + + public function testSuppressOn() + { + + $this->filter->suppress(true); + $this->assertFalse($this->filter->accept($this->logMessage1)); + $this->assertFalse($this->filter->accept($this->logMessage2)); + } + + public function testSuppressOff() + { + $this->filter->suppress(false); + $this->assertTrue($this->filter->accept($this->logMessage1)); + $this->assertTrue($this->filter->accept($this->logMessage2)); + } + + public function testSuppressCanBeReset() + { + $this->filter->suppress(true); + $this->assertFalse($this->filter->accept($this->logMessage1)); + $this->filter->suppress(false); + $this->assertTrue($this->filter->accept($this->logMessage2)); + $this->filter->suppress(true); + $this->assertFalse($this->filter->accept($this->logMessage3)); + } +} \ No newline at end of file diff --git a/test/Horde/Log/Filter/SuppressTest.php b/test/Filter/SuppressTest.php similarity index 90% rename from test/Horde/Log/Filter/SuppressTest.php rename to test/Filter/SuppressTest.php index bf2706c..6786872 100644 --- a/test/Horde/Log/Filter/SuppressTest.php +++ b/test/Filter/SuppressTest.php @@ -13,6 +13,9 @@ * @package Log * @subpackage UnitTests */ +namespace Horde\Log\Test\Filter; +use \PHPUnit\Framework\TestCase; +use \Horde_Log_Filter_Suppress; /** * @author Mike Naberezny @@ -22,9 +25,9 @@ * @package Log * @subpackage UnitTests */ -class Horde_Log_Filter_SuppressTest extends PHPUnit_Framework_TestCase +class SuppressTest extends TestCase { - public function setUp() + public function setUp(): void { $this->filter = new Horde_Log_Filter_Suppress(); } diff --git a/test/Formatter/CliFormatterTest.php b/test/Formatter/CliFormatterTest.php new file mode 100644 index 0000000..3d182f5 --- /dev/null +++ b/test/Formatter/CliFormatterTest.php @@ -0,0 +1,88 @@ + + * @category Horde + * @license http://www.horde.org/licenses/bsd BSD + * @package Log + */ + +namespace Horde\Log\Formatter\Test; + +use PHPUnit\Framework\TestCase; + +use Horde_Cli; +use Horde\Log\Formatter\CliFormatter; + +use Horde\Log\LogMessage; +use Horde\Log\LogLevel; + +class CliFormatterTest extends TestCase +{ + public function setUp(): void + { + $this->cli = new Horde_Cli(); + + $this->level1 = new LogLevel(1, 'Emergency'); + $this->level2 = new LogLevel(2, 'warning'); + $this->level3 = new LogLevel(3, 'info'); + $this->level4 = new LogLevel(4, 'Some other value'); + $this->message1 = 'this is an emergency!'; + $this->message2 = 'this is a warning!'; + $this->message3 = 'some info here!'; + $this->message4 = 'some other info here!'; + $this->logMessage1 = new LogMessage($this->level1, $this->message1); + $this->logMessage2 = new LogMessage($this->level2, $this->message2); + $this->logMessage3 = new LogMessage($this->level3, $this->message3); + $this->logMessage4 = new LogMessage($this->level4, $this->message4); + } + + public function testDefaultFormat() + { + $f = new CliFormatter($this->cli); + $line = $f->format($this->logMessage1); + + $loglevel = $this->logMessage1->level(); + $name = $loglevel->name(); + + # Note: the cliformatter does not output the value of "Criticallity" + // $criticality = $loglevel->criticality(); + + $this->assertStringContainsString($this->message1, $line); + $this->assertStringContainsString($name, $line); + } + + public function testColorSettings() + { + $f = new CliFormatter($this->cli); + $logsarray = [$this->logMessage1, $this->logMessage2, $this->logMessage3, $this->logMessage4]; + + foreach ($logsarray as $key => $value) { + $line = $f->format($value); + $loglevel = $value->level(); + $name = $loglevel->name(); + $logmessage = $value->message(); + $flag = '['. str_pad($name, 7, ' ', STR_PAD_BOTH) . '] '; + + switch ($name) { + case 'emergency': + $this->assertEquals($this->cli->color('red', $flag) . $logmessage, $line); + break; + case 'warning': + $this->assertEquals($this->cli->color('yellow', $flag) . $logmessage, $line); + break; + case 'info': + $this->assertEquals($this->cli->color('blue', $flag) . $logmessage, $line); + break; + default: + $this->assertEquals($flag . $logmessage, $line); + break; + } + } + } +} diff --git a/test/Formatter/SimpleFormatterTest.php b/test/Formatter/SimpleFormatterTest.php new file mode 100644 index 0000000..87540b9 --- /dev/null +++ b/test/Formatter/SimpleFormatterTest.php @@ -0,0 +1,40 @@ + $context ]); + + $logMessage->formatMessage([]); + + $line = $f->format($logMessage); + + /*var_dump ($line);*/ + + $this->assertEquals('test message testValue', $line); + + } +} \ No newline at end of file diff --git a/test/Horde/Log/Formatter/SimpleTest.php b/test/Formatter/SimpleTest.php similarity index 70% rename from test/Horde/Log/Formatter/SimpleTest.php rename to test/Formatter/SimpleTest.php index 37a3c62..28cc377 100644 --- a/test/Horde/Log/Formatter/SimpleTest.php +++ b/test/Formatter/SimpleTest.php @@ -13,6 +13,10 @@ * @package Log * @subpackage UnitTests */ +namespace Horde\Log\Test; +use \PHPUnit\Framework\TestCase; +use \Horde_Log; +use \Horde_Log_Formatter_Simple; /** * @author Mike Naberezny @@ -22,17 +26,12 @@ * @package Log * @subpackage UnitTests */ -class Horde_Log_Formatter_SimpleTest extends PHPUnit_Framework_TestCase +class SimpleTest extends TestCase { public function testConstructorThrowsOnBadFormatString() { - try { - new Horde_Log_Formatter_Simple(1); - $this->fail(); - } catch (Exception $e) { - $this->assertInstanceOf('InvalidArgumentException', $e); - $this->assertRegExp('/must be a string/i', $e->getMessage()); - } + $this->expectException('InvalidArgumentException'); + new Horde_Log_Formatter_Simple(1); } public function testDefaultFormat() @@ -42,7 +41,7 @@ public function testDefaultFormat() 'level' => $level = Horde_Log::ALERT, 'levelName' => $levelName = 'ALERT')); - $this->assertContains($message, $line); - $this->assertContains($levelName, $line); + $this->assertStringContainsString($message, $line); + $this->assertStringContainsString($levelName, $line); } } diff --git a/test/Horde/Log/Formatter/XmlTest.php b/test/Formatter/XmlTest.php similarity index 79% rename from test/Horde/Log/Formatter/XmlTest.php rename to test/Formatter/XmlTest.php index 4857ff8..8c7955f 100644 --- a/test/Horde/Log/Formatter/XmlTest.php +++ b/test/Formatter/XmlTest.php @@ -12,6 +12,9 @@ * @license http://www.horde.org/licenses/bsd BSD * @package Log */ +namespace Horde\Log\Formatter\Test; +use \PHPUnit\Framework\TestCase; +use \Horde_Log_Formatter_Xml; /** * @author Mike Naberezny @@ -20,9 +23,9 @@ * @license http://www.horde.org/licenses/bsd BSD * @package Log */ -class Horde_Log_Formatter_XmlTest extends PHPUnit_Framework_TestCase +class XmlTest extends TestCase { - public function setUp() + public function setUp(): void { date_default_timezone_set('America/New_York'); } @@ -32,8 +35,8 @@ public function testDefaultFormat() $f = new Horde_Log_Formatter_Xml(); $line = $f->format(array('message' => $message = 'message', 'level' => $level = 1)); - $this->assertContains($message, $line); - $this->assertContains((string)$level, $line); + $this->assertStringContainsString($message, $line); + $this->assertStringContainsString((string)$level, $line); } public function testXmlDeclarationIsStripped() @@ -41,7 +44,7 @@ public function testXmlDeclarationIsStripped() $f = new Horde_Log_Formatter_Xml(); $line = $f->format(array('message' => $message = 'message', 'level' => $level = 1)); - $this->assertNotContains('<\?xml version=', $line); + $this->assertStringNotContainsString('<\?xml version=', $line); } public function testXmlValidates() diff --git a/test/Horde/Log/Handler/FirebugTest.php b/test/Handler/FirebugTest.php similarity index 71% rename from test/Horde/Log/Handler/FirebugTest.php rename to test/Handler/FirebugTest.php index 7a7a7ec..286b56d 100644 --- a/test/Horde/Log/Handler/FirebugTest.php +++ b/test/Handler/FirebugTest.php @@ -13,6 +13,11 @@ * @package Log * @subpackage UnitTests */ +namespace Horde\Log\Test\Handler; +use \PHPUnit\Framework\TestCase; +use \Horde_Log; +use \Horde_Log_Handler_Stream; +use \Horde_Log_Handler_Firebug; /** * @author Mike Naberezny @@ -22,23 +27,18 @@ * @package Log * @subpackage UnitTests */ -class Horde_Log_Handler_FirebugTest extends PHPUnit_Framework_TestCase +class FirebugTest extends TestCase { - public function setUp() + public function setUp(): void { date_default_timezone_set('America/New_York'); } public function testSettingBadOptionThrows() { - try { - $handler = new Horde_Log_Handler_Stream('php://memory'); - $handler->setOption('foo', 42); - $this->fail(); - } catch (Exception $e) { - $this->assertInstanceOf('Horde_Log_Exception', $e); - $this->assertRegExp('/unknown option/i', $e->getMessage()); - } + $this->expectException('Horde_Log_Exception'); + $handler = new Horde_Log_Handler_Stream('php://memory'); + $handler->setOption('foo', 42); } public function testWrite() @@ -55,7 +55,7 @@ public function testWrite() $date = '\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}-\d{2}:\d{2}'; - $this->assertRegExp("/console.error\(\"$date $levelName: $message\"\);/", $contents); + $this->assertMatchesRegularExpression("/console.error\(\"$date $levelName: $message\"\);/", $contents); } } diff --git a/test/Horde/Log/Handler/NullTest.php b/test/Handler/NullTest.php similarity index 86% rename from test/Horde/Log/Handler/NullTest.php rename to test/Handler/NullTest.php index 64bf442..20d1e2e 100644 --- a/test/Horde/Log/Handler/NullTest.php +++ b/test/Handler/NullTest.php @@ -13,6 +13,9 @@ * @package Log * @subpackage UnitTests */ +namespace Horde\Log\Test\Handler; +use \PHPUnit\Framework\TestCase; +use \Horde_Log_Handler_Null; /** * @author Mike Naberezny @@ -22,7 +25,7 @@ * @package Log * @subpackage UnitTests */ -class Horde_Log_Handler_NullTest extends PHPUnit_Framework_TestCase +class NullTest extends TestCase { public function testWrite() { diff --git a/test/Horde/Log/Handler/StreamTest.php b/test/Handler/StreamTest.php similarity index 55% rename from test/Horde/Log/Handler/StreamTest.php rename to test/Handler/StreamTest.php index 0759b20..e002c50 100644 --- a/test/Horde/Log/Handler/StreamTest.php +++ b/test/Handler/StreamTest.php @@ -13,6 +13,10 @@ * @package Log * @subpackage UnitTests */ +namespace Horde\Log\Test\Handler; +use \PHPUnit\Framework\TestCase; +use \Horde_Log_Handler_Stream; +use \Horde_Log; /** * @author Mike Naberezny @@ -22,70 +26,56 @@ * @package Log * @subpackage UnitTests */ -class Horde_Log_Handler_StreamTest extends PHPUnit_Framework_TestCase +class StreamTest extends TestCase { - public function setUp() + public function setUp(): void { date_default_timezone_set('America/New_York'); } public function testConstructorThrowsWhenResourceIsNotStream() { + $this->expectException('Horde_Log_Exception'); $resource = xml_parser_create(); - try { - new Horde_Log_Handler_Stream($resource); - $this->fail(); - } catch (Exception $e) { - $this->assertInstanceOf('Horde_Log_Exception', $e); - $this->assertRegExp('/not a stream/i', $e->getMessage()); - } + new Horde_Log_Handler_Stream($resource); xml_parser_free($resource); } public function testConstructorWithValidStream() { $stream = fopen('php://memory', 'a'); - new Horde_Log_Handler_Stream($stream); + $this->assertInstanceOf( + 'Horde_Log_Handler_Base', + new Horde_Log_Handler_Stream($stream) + ); } public function testConstructorWithValidUrl() { - new Horde_Log_Handler_Stream('php://memory'); + $this->assertInstanceOf( + 'Horde_Log_Handler_Base', + new Horde_Log_Handler_Stream('php://memory') + ); } public function testConstructorThrowsWhenModeSpecifiedForExistingStream() { + $this->expectException('Horde_Log_Exception'); $stream = fopen('php://memory', 'a'); - try { - new Horde_Log_Handler_Stream($stream, 'w'); - $this->fail(); - } catch (Exception $e) { - $this->assertInstanceOf('Horde_Log_Exception', $e); - $this->assertRegExp('/existing stream/i', $e->getMessage()); - } + new Horde_Log_Handler_Stream($stream, 'w'); } public function testConstructorThrowsWhenStreamCannotBeOpened() { - try { - new Horde_Log_Handler_Stream(''); - $this->fail(); - } catch (Exception $e) { - $this->assertInstanceOf('Horde_Log_Exception', $e); - $this->assertRegExp('/cannot be opened/i', $e->getMessage()); - } + $this->expectException('Horde_Log_Exception'); + new Horde_Log_Handler_Stream(''); } public function testSettingBadOptionThrows() { - try { - $handler = new Horde_Log_Handler_Stream('php://memory'); - $handler->setOption('foo', 42); - $this->fail(); - } catch (Exception $e) { - $this->assertInstanceOf('Horde_Log_Exception', $e); - $this->assertRegExp('/unknown option/i', $e->getMessage()); - } + $this->expectException('Horde_Log_Exception'); + $handler = new Horde_Log_Handler_Stream('php://memory'); + $handler->setOption('foo', 42); } public function testWrite() @@ -104,22 +94,16 @@ public function testWrite() $date = '\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}-\d{2}:\d{2}'; - $this->assertRegExp("/$date $levelName: $message/", $contents); + $this->assertMatchesRegularExpression("/$date $levelName: $message/", $contents); } public function testWriteThrowsWhenStreamWriteFails() { + $this->expectException('Horde_Log_Exception'); $stream = fopen('php://memory', 'a'); $handler = new Horde_Log_Handler_Stream($stream); fclose($stream); - - try { - $handler->write(array('message' => 'foo', 'level' => 1)); - $this->fail(); - } catch (Exception $e) { - $this->assertInstanceOf('Horde_Log_Exception', $e); - $this->assertRegExp('/unable to write/i', $e->getMessage()); - } + $handler->write(array('message' => 'foo', 'level' => 1)); } } diff --git a/test/Horde/Log/AllTests.php b/test/Horde/Log/AllTests.php deleted file mode 100644 index 49583c8..0000000 --- a/test/Horde/Log/AllTests.php +++ /dev/null @@ -1,3 +0,0 @@ -run(); diff --git a/test/Horde/Log/bootstrap.php b/test/Horde/Log/bootstrap.php deleted file mode 100644 index 4e19e93..0000000 --- a/test/Horde/Log/bootstrap.php +++ /dev/null @@ -1,3 +0,0 @@ - @@ -22,9 +27,9 @@ * @package Log * @subpackage UnitTests */ -class Horde_Log_LogTest extends PHPUnit_Framework_TestCase +class LogTest extends TestCase { - public function setUp() + public function setUp(): void { date_default_timezone_set('America/New_York'); @@ -40,7 +45,7 @@ public function testHandlerCanBeAddedWithConstructor() $logger->log($message = 'message-to-long', Horde_Log::INFO); rewind($this->log); - $this->assertContains($message, stream_get_contents($this->log)); + $this->assertStringContainsString($message, stream_get_contents($this->log)); } public function testaddHandler() @@ -50,7 +55,7 @@ public function testaddHandler() $logger->log($message = 'message-to-log', Horde_Log::INFO); rewind($this->log); - $this->assertContains($message, stream_get_contents($this->log)); + $this->assertStringContainsString($message, stream_get_contents($this->log)); } public function testaddHandlerAddsMultipleHandlers() @@ -72,9 +77,9 @@ public function testaddHandlerAddsMultipleHandlers() // verify both handlers were called by the logger rewind($log1); - $this->assertContains($message, stream_get_contents($log1)); + $this->assertStringContainsString($message, stream_get_contents($log1)); rewind($log2); - $this->assertContains($message, stream_get_contents($log2)); + $this->assertStringContainsString($message, stream_get_contents($log2)); // prove the two memory streams are different // and both handlers were indeed called @@ -84,52 +89,32 @@ public function testaddHandlerAddsMultipleHandlers() public function testLoggerThrowsWhenNoHandlers() { + $this->expectException('Horde_Log_Exception'); $logger = new Horde_Log_Logger(); - try { - $logger->log('message', Horde_Log::INFO); - $this->fail(); - } catch (Horde_Log_Exception $e) { - $this->assertRegexp('/no handler/i', $e->getMessage()); - } + $logger->log('message', Horde_Log::INFO); } // Levels public function testLogThrowsOnBadLogLevel() { + $this->expectException('Horde_Log_Exception'); $logger = new Horde_Log_Logger($this->handler); - try { - $logger->log('foo', 42); - $this->fail(); - } catch (Exception $e) { - $this->assertInstanceOf('Horde_Log_Exception', $e); - $this->assertRegExp('/bad log level/i', $e->getMessage()); - } + $logger->log('foo', 42); } public function testLogThrough__callThrowsOnBadLogLevel() { + $this->expectException('Horde_Log_Exception'); $logger = new Horde_Log_Logger($this->handler); - try { - $logger->nonexistantLevel(''); - $this->fail(); - } catch (Exception $e) { - $this->assertInstanceOf('Horde_Log_Exception', $e); - $this->assertRegExp('/bad log level/i', $e->getMessage()); - } + $logger->nonexistantLevel(''); } public function testAddingLevelThrowsWhenOverridingBuiltinLogLevel() { - try { - $logger = new Horde_Log_Logger($this->handler); - $logger->addLevel('WARN', 99); - $this->fail(); - } catch (Exception $e) { - $this->assertInstanceOf('Horde_Log_Exception', $e); - $this->assertRegExp('/existing log level/i', $e->getMessage()); - } - + $this->expectException('Horde_Log_Exception'); + $logger = new Horde_Log_Logger($this->handler); + $logger->addLevel('WARN', 99); } public function testAddLogLevel() @@ -141,7 +126,7 @@ public function testAddLogLevel() rewind($this->log); $logdata = stream_get_contents($this->log); - $this->assertContains($levelName, $logdata); - $this->assertContains($message, $logdata); + $this->assertStringContainsString($levelName, $logdata); + $this->assertStringContainsString($message, $logdata); } } diff --git a/test/bootstrap.php b/test/bootstrap.php new file mode 100644 index 0000000..bde1685 --- /dev/null +++ b/test/bootstrap.php @@ -0,0 +1,12 @@ +