├── .github └── workflows │ ├── test_develop_and_main.yml │ ├── test_other_branches.yml │ └── test_pull_request.yml ├── .gitignore ├── CHANGELOG.md ├── LICENSE.txt ├── Makefile ├── README.md ├── composer.json ├── docs ├── demo.php └── image │ └── SimpleLogLogo.png ├── src └── Logger.php └── tests ├── Fixture └── StringableMessage.php ├── LoggerTest.php ├── bootstrap.php ├── coding_standard.xml ├── phpstan.neon ├── phpunit.xml └── psalm.xml /.github/workflows/test_develop_and_main.yml: -------------------------------------------------------------------------------- 1 | name: Test, Lint and Static Analysis (Develop and Main) 2 | 3 | on: 4 | push: 5 | branches: 6 | - develop 7 | - main 8 | 9 | jobs: 10 | test-lint-and-static-analysis: 11 | name: Test, Lint and Static Analysis 12 | runs-on: ubuntu-latest 13 | strategy: 14 | matrix: 15 | php: ['8.0', '8.1', '8.2', '8.3'] 16 | 17 | steps: 18 | - name: Set up PHP 19 | uses: shivammathur/setup-php@v2 20 | with: 21 | php-version: ${{ matrix.php }} 22 | tools: composer:v2 23 | 24 | - name: Set up Node 25 | uses: actions/setup-node@v4 26 | with: 27 | node-version: '20.x' 28 | 29 | - name: Checkout code 30 | uses: actions/checkout@v4 31 | with: 32 | fetch-depth: 0 33 | 34 | - name: PHP Version Check 35 | run: php -v 36 | 37 | - name: Validate Composer JSON 38 | run: composer validate 39 | 40 | - name: Run Composer 41 | run: composer install --no-interaction 42 | 43 | - name: PHP Lint - Syntax Linting 44 | run: ./vendor/bin/parallel-lint src tests 45 | 46 | - name: PHPUnit - Unit Tests 47 | run: | 48 | mkdir -p build/logs 49 | ./vendor/bin/phpunit --version 50 | ./vendor/bin/phpunit --configuration tests/phpunit.xml 51 | 52 | - name: PHP Code Sniffer - Style Linting 53 | run: | 54 | ./vendor/bin/phpcs --version 55 | ./vendor/bin/phpcs --ignore=vendor --standard=tests/coding_standard.xml -s . 56 | 57 | - name: PHP Stan - Static Analysis 58 | run: | 59 | ./vendor/bin/phpstan --version 60 | ./vendor/bin/phpstan analyze -c tests/phpstan.neon 61 | 62 | - name: Psalm - Static Analysis 63 | run: | 64 | ./vendor/bin/psalm --version 65 | ./vendor/bin/psalm --config=tests/psalm.xml 66 | 67 | code-coverage: 68 | name: Code coverage 69 | runs-on: ubuntu-latest 70 | strategy: 71 | matrix: 72 | php: ['8.0'] 73 | 74 | steps: 75 | - name: Set up PHP 76 | uses: shivammathur/setup-php@v2 77 | with: 78 | php-version: ${{ matrix.php }} 79 | coverage: xdebug 80 | tools: composer:v2 81 | 82 | - name: Set up Node 83 | uses: actions/setup-node@v4 84 | with: 85 | node-version: '20.x' 86 | 87 | - name: Checkout code 88 | uses: actions/checkout@v4 89 | with: 90 | fetch-depth: 0 91 | 92 | - name: Run Composer 93 | run: composer install --no-interaction 94 | 95 | - name: Unit tests 96 | run: | 97 | mkdir -p build/logs 98 | ./vendor/bin/phpunit --configuration tests/phpunit.xml --coverage-clover build/logs/clover.xml 99 | 100 | - name: Code Coverage (Coveralls) 101 | env: 102 | COVERALLS_REPO_TOKEN: ${{ secrets.GITHUB_TOKEN }} 103 | run: php vendor/bin/php-coveralls -v 104 | -------------------------------------------------------------------------------- /.github/workflows/test_other_branches.yml: -------------------------------------------------------------------------------- 1 | name: Test and Static Analysis (Other Branches) 2 | 3 | on: 4 | push: 5 | branches-ignore: 6 | - develop 7 | - main 8 | 9 | jobs: 10 | test-lint-and-static-analysis: 11 | name: Test, Lint and Static Analysis 12 | runs-on: ubuntu-latest 13 | strategy: 14 | matrix: 15 | php: ['8.0', '8.1', '8.2', '8.3'] 16 | 17 | steps: 18 | - name: Set up PHP 19 | uses: shivammathur/setup-php@v2 20 | with: 21 | php-version: ${{ matrix.php }} 22 | tools: composer:v2 23 | 24 | - name: Set up Node 25 | uses: actions/setup-node@v4 26 | with: 27 | node-version: '20.x' 28 | 29 | - name: Checkout code 30 | uses: actions/checkout@v4 31 | with: 32 | fetch-depth: 0 33 | 34 | - name: PHP Version Check 35 | run: php -v 36 | 37 | - name: Validate Composer JSON 38 | run: composer validate 39 | 40 | - name: Run Composer 41 | run: composer install --no-interaction 42 | 43 | - name: PHP Lint - Syntax Linting 44 | run: ./vendor/bin/parallel-lint src tests 45 | 46 | - name: PHPUnit - Unit Tests 47 | run: | 48 | mkdir -p build/logs 49 | ./vendor/bin/phpunit --version 50 | ./vendor/bin/phpunit --configuration tests/phpunit.xml 51 | 52 | - name: PHP Code Sniffer - Style Linting 53 | run: | 54 | ./vendor/bin/phpcs --version 55 | ./vendor/bin/phpcs --ignore=vendor --standard=tests/coding_standard.xml -s . 56 | 57 | - name: PHP Stan - Static Analysis 58 | run: | 59 | ./vendor/bin/phpstan --version 60 | ./vendor/bin/phpstan analyze -c tests/phpstan.neon 61 | 62 | - name: Psalm - Static Analysis 63 | run: | 64 | ./vendor/bin/psalm --version 65 | ./vendor/bin/psalm --config=tests/psalm.xml -------------------------------------------------------------------------------- /.github/workflows/test_pull_request.yml: -------------------------------------------------------------------------------- 1 | name: Test and Static Analysis (Pull Request) 2 | 3 | on: pull_request 4 | 5 | jobs: 6 | test-lint-and-static-analysis: 7 | name: Test, Lint and Static Analysis 8 | runs-on: ubuntu-latest 9 | strategy: 10 | matrix: 11 | php: ['8.0', '8.1', '8.2', '8.3'] 12 | 13 | steps: 14 | - name: Set up PHP 15 | uses: shivammathur/setup-php@v2 16 | with: 17 | php-version: ${{ matrix.php }} 18 | tools: composer:v2 19 | 20 | - name: Set up Node 21 | uses: actions/setup-node@v4 22 | with: 23 | node-version: '20.x' 24 | 25 | - name: Checkout code 26 | uses: actions/checkout@v4 27 | with: 28 | fetch-depth: 0 29 | 30 | - name: PHP Version Check 31 | run: php -v 32 | 33 | - name: Validate Composer JSON 34 | run: composer validate 35 | 36 | - name: Run Composer 37 | run: composer install --no-interaction 38 | 39 | - name: PHP Lint - Syntax Linting 40 | run: ./vendor/bin/parallel-lint src tests 41 | 42 | - name: PHPUnit - Unit Tests 43 | run: | 44 | mkdir -p build/logs 45 | ./vendor/bin/phpunit --version 46 | ./vendor/bin/phpunit --configuration tests/phpunit.xml 47 | 48 | - name: PHP Code Sniffer - Style Linting 49 | run: | 50 | ./vendor/bin/phpcs --version 51 | ./vendor/bin/phpcs --ignore=vendor --standard=tests/coding_standard.xml -s . 52 | 53 | - name: PHP Stan - Static Analysis 54 | run: | 55 | ./vendor/bin/phpstan --version 56 | ./vendor/bin/phpstan analyze -c tests/phpstan.neon 57 | 58 | - name: Psalm - Static Analysis 59 | run: | 60 | ./vendor/bin/psalm --version 61 | ./vendor/bin/psalm --config=tests/psalm.xml 62 | 63 | code-coverage: 64 | name: Code coverage 65 | runs-on: ubuntu-latest 66 | strategy: 67 | matrix: 68 | php: ['8.0'] 69 | 70 | steps: 71 | - name: Set up PHP 72 | uses: shivammathur/setup-php@v2 73 | with: 74 | php-version: ${{ matrix.php }} 75 | coverage: xdebug 76 | tools: composer:v2 77 | 78 | - name: Set up Node 79 | uses: actions/setup-node@v4 80 | with: 81 | node-version: '20.x' 82 | 83 | - name: Checkout code 84 | uses: actions/checkout@v4 85 | with: 86 | fetch-depth: 0 87 | 88 | - name: Run Composer 89 | run: composer install --no-interaction 90 | 91 | - name: Unit tests 92 | run: | 93 | mkdir -p build/logs 94 | ./vendor/bin/phpunit --configuration tests/phpunit.xml --coverage-clover build/logs/clover.xml 95 | 96 | - name: Code Coverage (Coveralls) 97 | env: 98 | COVERALLS_REPO_TOKEN: ${{ secrets.GITHUB_TOKEN }} 99 | run: php vendor/bin/php-coveralls -v -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | /vendor 2 | composer.lock 3 | composer.phar 4 | /.idea 5 | .phpunit.result.cache 6 | 7 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # SimpleLog Change Log 2 | 3 | ## v2.1.0 - 2024-11-05 4 | 5 | - Add `psr/log` 3.0.* support. 6 | - `psr/log` 2.0.* still supported with Composer `||` conditional 7 | 8 | ## v2.0.0 - 2024-02-25 9 | 10 | Updated for PHP and PSR log version updates. 11 | 12 | - PHP minimum version 8.0. 13 | - Implements `psr/log` 2.0.* 14 | 15 | ## v1.0.0 - 2023-09-09 16 | 17 | First official release. 18 | 19 | - PHP minimum version 7.4. 20 | - Implements `psr/log` 1.1.* 21 | 22 | ## v0.4.0 - 2019-08-10 23 | 24 | First stable release. 25 | 26 | - PHP minimum version 7.0. 27 | - Implements `psr/log` 1.1.* 28 | -------------------------------------------------------------------------------- /LICENSE.txt: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2017 Mark Rogoyski 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | .PHONY : lint tests style phpstan psalm phpmd report coverage demo 2 | 3 | all : lint tests style phpstan psalm demo 4 | 5 | tests : 6 | vendor/bin/phpunit tests/ --configuration=tests/phpunit.xml 7 | 8 | lint : 9 | vendor/bin/parallel-lint src tests 10 | 11 | style : 12 | vendor/bin/phpcs --standard=tests/coding_standard.xml --ignore=vendor -s . 13 | 14 | phpstan : 15 | vendor/bin/phpstan analyze -c tests/phpstan.neon 16 | 17 | psalm : 18 | vendor/bin/psalm --config=tests/psalm.xml 19 | 20 | phpmd : 21 | vendor/bin/phpmd src/ ansi cleancode,codesize,design,unusedcode,naming 22 | 23 | coverage : 24 | vendor/bin/phpunit tests/ --configuration=tests/phpunit.xml --coverage-text=php://stdout 25 | 26 | report : 27 | vendor/bin/phploc src/ 28 | 29 | demo : 30 | php docs/demo.php 31 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ![SimpleLog Logo](https://github.com/markrogoyski/simplelog-php/blob/master/docs/image/SimpleLogLogo.png?raw=true) 2 | 3 | SimpleLog 4 | ===================== 5 | 6 | ### Powerful PSR-3 logging. So easy, it's simple! 7 | 8 | SimpleLog is a powerful PSR-3 logger for PHP that is simple to use. 9 | 10 | Simplicity is achieved by providing great defaults. No options to configure! Yet flexible enough to meet most logging needs. 11 | And if your application's logging needs expand beyond what SimpleLog provides, since it implements PSR-3, you can drop in 12 | another great PSR-3 logger like [MonoLog](https://github.com/Seldaek/monolog) in its place when the time comes with minimal changes. 13 | 14 | [![Coverage Status](https://coveralls.io/repos/github/markrogoyski/simplelog-php/badge.svg?branch=master)](https://coveralls.io/github/markrogoyski/simplelog-php?branch=master) 15 | [![License](https://poser.pugx.org/markrogoyski/simplelog-php/license)](https://packagist.org/packages/markrogoyski/simplelog-php) 16 | 17 | Features 18 | -------- 19 | 20 | * Power and Simplicity 21 | * PSR-3 logger interface 22 | * Multiple log level severities 23 | * Log channels 24 | * Process ID logging 25 | * Custom log messages 26 | * Custom contextual data 27 | * Exception logging 28 | 29 | Setup 30 | ----- 31 | 32 | Add the library to your `composer.json` file in your project: 33 | 34 | ```javascript 35 | { 36 | "require": { 37 | "markrogoyski/simplelog-php": "2.*" 38 | } 39 | } 40 | ``` 41 | 42 | Use [composer](http://getcomposer.org) to install the library: 43 | 44 | ```bash 45 | $ php composer.phar install 46 | ``` 47 | 48 | Composer will install SimpleLog inside your vendor folder. Then you can add the following to your 49 | .php files to use the library with Autoloading. 50 | 51 | ```php 52 | require_once(__DIR__ . '/vendor/autoload.php'); 53 | ``` 54 | 55 | Alternatively, use composer on the command line to require and install SimpleLog: 56 | 57 | ``` 58 | $ php composer.phar require markrogoyski/simplelog-php:2.* 59 | ``` 60 | 61 | ### Minimum Requirements 62 | * PHP 8.0 63 | 64 | - **Note**: For PHP 7.4, use v1.0 (`markrogoyski/simplelog-php:1.0`) 65 | - **Note**: For PHP 7.0–7.3, use v0.4 (`markrogoyski/simplelog-php:0.4`) 66 | 67 | Usage 68 | ----- 69 | 70 | ### Simple 20-Second Getting-Started Tutorial 71 | ```php 72 | $logfile = '/path/to/logfile.log'; 73 | $channel = 'events'; 74 | $logger = new SimpleLog\Logger($logfile, $channel); 75 | 76 | $logger->info('SimpleLog really is simple.'); 77 | ``` 78 | 79 | That's it! Your application is logging! 80 | 81 | ### Extended Example 82 | ```php 83 | $logfile = '/var/log/events.log'; 84 | $channel = 'billing'; 85 | $logger = new SimpleLog\Logger($logfile, $channel); 86 | 87 | $logger->info('Begin process that usually fails.', ['process' => 'invoicing', 'user' => $user]); 88 | 89 | try { 90 | invoiceUser($user); // This usually fails 91 | } catch (\Exception $e) { 92 | $logger->error('Billing failure.', ['process' => 'invoicing', 'user' => $user, 'exception' => $e]); 93 | } 94 | ``` 95 | 96 | Logger output 97 | ``` 98 | 2017-02-13 00:35:55.426630 [info] [billing] [pid:17415] Begin process that usually fails. {"process":"invoicing","user":"bob"} {} 99 | 2017-02-13 00:35:55.430071 [error] [billing] [pid:17415] Billing failure. {"process":"invoicing","user":"bob"} {"message":"Could not process invoice.","code":0,"file":"/path/to/app.php","line":20,"trace":[{"file":"/path/to/app.php","line":13,"function":"invoiceUser","args":["mark"]}]} 100 | ``` 101 | 102 | ### Log Output 103 | Log lines have the following format: 104 | ``` 105 | YYYY-mm-dd HH:ii:ss.uuuuuu [loglevel] [channel] [pid:##] Log message content {"Optional":"JSON Contextual Support Data"} {"Optional":"Exception Data"} 106 | ``` 107 | 108 | Log lines are easily readable and parsable. Log lines are always on a single line. Fields are tab separated. 109 | 110 | ### Log Levels 111 | 112 | SimpleLog has eight log level severities based on [PSR Log Levels](http://www.php-fig.org/psr/psr-3/#psrlogloglevel). 113 | 114 | ```php 115 | $logger->debug('Detailed information about the application run.'); 116 | $logger->info('Informational messages about the application run.'); 117 | $logger->notice('Normal but significant events.'); 118 | $logger->warning('Information that something potentially bad has occured.'); 119 | $logger->error('Runtime error that should be monitored.'); 120 | $logger->critical('A service is unavailable or unresponsive.'); 121 | $logger->alert('The entire site is down.'); 122 | $logger->emergency('The Web site is on fire.'); 123 | ``` 124 | 125 | By default all log levels are logged. The minimum log level can be changed in two ways: 126 | * Optional constructor parameter 127 | * Setter method at any time 128 | 129 | ```php 130 | use Psr\Log\LogLevel; 131 | 132 | // Optional constructor Parameter (Only error and above are logged [error, critical, alert, emergency]) 133 | $logger = new SimpleLog\Logger($logfile, $channel, LogLevel::ERROR); 134 | 135 | // Setter method (Only warning and above are logged) 136 | $logger->setLogLevel(LogLevel::WARNING); 137 | ``` 138 | 139 | ### Contextual Data 140 | SimpleLog enables logging best practices to have general-use log messages with contextual support data to give context to the message. 141 | 142 | The second argument to a log message is an associative array of key-value pairs that will log as a JSON string, serving as the contextual support data to the log message. 143 | 144 | ```php 145 | // Add context to a Web request. 146 | $log->info('Web request initiated', ['method' => 'GET', 'endpoint' => 'user/account', 'queryParameters' => 'id=1234']); 147 | 148 | // Add context to a disk space warning. 149 | $log->warning('Free space is below safe threshold.', ['volume' => '/var/log', 'availablePercent' => 4]); 150 | ``` 151 | 152 | ### Logging Exceptions 153 | Exceptions are logged with the contextual data using the key *exception* and the value the exception variable. 154 | 155 | ```php 156 | catch (\Exception $e) { 157 | $logger->error('Something exceptional has happened', ['exception' => $e]); 158 | } 159 | ``` 160 | 161 | ### Log Channels 162 | Think of channels as namespaces for log lines. If you want to have multiple loggers or applications logging to a single log file, channels are your friend. 163 | 164 | Channels can be set in two ways: 165 | * Constructor parameter 166 | * Setter method at any time 167 | 168 | ```php 169 | // Constructor Parameter 170 | $channel = 'router'; 171 | $logger = new SimpleLog\Logger($logfile, $channel); 172 | 173 | // Setter method 174 | $logger->setChannel('database'); 175 | ``` 176 | 177 | ### Debug Features 178 | #### Logging to STDOUT 179 | When developing, you can turn on log output to the screen (STDOUT) as a convenience. 180 | 181 | ```php 182 | $logger->setOutput(true); 183 | $logger->debug('This will get logged to STDOUT as well as the log file.'); 184 | ``` 185 | 186 | #### Dummy Logger 187 | Suppose you need a logger to meet an injected dependency during a unit test, and you don't want it to actually log anything. 188 | You can set the log level to ```Logger::LOG_LEVEL_NONE``` which won't log at any level. 189 | 190 | ```php 191 | use SimpleLog\Logger; 192 | 193 | $logger->setLogLevel(Logger::LOG_LEVEL_NONE); 194 | $logger->info('This will not log to a file.'); 195 | ``` 196 | 197 | Unit Tests 198 | ---------- 199 | 200 | ```bash 201 | $ cd tests 202 | $ phpunit 203 | ``` 204 | 205 | [![Coverage Status](https://coveralls.io/repos/github/markrogoyski/simplelog-php/badge.svg?branch=master)](https://coveralls.io/github/markrogoyski/simplelog-php?branch=master) 206 | 207 | Standards 208 | --------- 209 | 210 | SimpleLog conforms to the following standards: 211 | 212 | * PSR-1 - Basic coding standard (http://www.php-fig.org/psr/psr-1/) 213 | * PSR-3 - Logger Interface (http://www.php-fig.org/psr/psr-3/) 214 | * PSR-4 - Autoloader (http://www.php-fig.org/psr/psr-4/) 215 | * PSR-12 - Extended coding style guide (http://www.php-fig.org/psr/psr-12/) 216 | 217 | License 218 | ------- 219 | 220 | SimpleLog is licensed under the MIT License. 221 | -------------------------------------------------------------------------------- /composer.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "markrogoyski/simplelog-php", 3 | "type": "library", 4 | "description": "Powerful PSR-3 logging. So easy, it's simple.", 5 | "keywords": ["log", "logging", "psr-3", "logger"], 6 | "homepage": "https://github.com/markrogoyski/simplelog-php/", 7 | "require": { 8 | "php": ">=8.0.0", 9 | "psr/log": "2.0.* || 3.0.*", 10 | "ext-json": "*" 11 | }, 12 | "require-dev": { 13 | "phpunit/phpunit": "^9.0", 14 | "php-coveralls/php-coveralls": "^2.7", 15 | "squizlabs/php_codesniffer": "^3.10", 16 | "phpstan/phpstan": "^1.12", 17 | "vimeo/psalm": "^5.26", 18 | "phpmd/phpmd": "^2.10", 19 | "phploc/phploc": "*", 20 | "php-parallel-lint/php-parallel-lint": "^1.4" 21 | }, 22 | "autoload": { 23 | "psr-4": {"SimpleLog\\": "src/"} 24 | }, 25 | "autoload-dev": { 26 | "psr-4": { "SimpleLog\\Tests\\": "tests/" } 27 | }, 28 | "authors": [ 29 | { 30 | "name": "Mark Rogoyski", 31 | "email": "mark@rogoyski.com", 32 | "homepage": "https://github.com/markrogoyski", 33 | "role": "Lead Developer" 34 | } 35 | ], 36 | "license": "MIT" 37 | } 38 | -------------------------------------------------------------------------------- /docs/demo.php: -------------------------------------------------------------------------------- 1 | setOutput(true); 16 | 17 | // Logging at different log levels without context. 18 | $logger->debug('This is a debug message.'); 19 | $logger->info('This is an info message.'); 20 | $logger->notice('This is a notice message.'); 21 | $logger->warning('This is a warning message.'); 22 | $logger->error('This is an error message.'); 23 | $logger->critical('This is a critical message.'); 24 | $logger->alert('This is an alert message.'); 25 | $logger->emergency('This is an emergency message.'); 26 | 27 | // Logging with context 28 | $logger->info('This is an info message with context.', ['method' => 'GET', 'endpoint' => '/v2/demo']); 29 | 30 | // Logging with context that includes an exception. 31 | $e = new \RuntimeException('KaPoW! Exception Message'); 32 | $logger->error('Something bad happened', ['exception' => $e, 'endpoint' => '/v2/demo']); 33 | -------------------------------------------------------------------------------- /docs/image/SimpleLogLogo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/markrogoyski/simplelog-php/3a8fc992b0779c6dfd7d8dbec7694fcafec0c768/docs/image/SimpleLogLogo.png -------------------------------------------------------------------------------- /src/Logger.php: -------------------------------------------------------------------------------- 1 | info('Normal informational event happened.'); 18 | * $logger->error('Something bad happened.', ['key1' => 'value that gives context', 'key2' => 'some more context', 'exception' => $e]); 19 | * 20 | * Optional constructor option: Set default lowest log level (Example error and above): 21 | * $logger = new SimpleLog\Logger('logfile.log', 'channelname', \Psr\Log\LogLevel::ERROR); 22 | * $logger->error('This will get logged'); 23 | * $logger->info('This is below the minimum log level and will not get logged'); 24 | * 25 | * To log an exception, set as data context array key 'exception' 26 | * $logger->error('Something exceptional happened.', ['exception' => $e]); 27 | * 28 | * To set output to standard out (STDOUT) as well as a log file: 29 | * $logger->setOutput(true); 30 | * 31 | * To change the channel after construction: 32 | * $logger->setChannel('newname') 33 | */ 34 | class Logger implements \Psr\Log\LoggerInterface 35 | { 36 | /** 37 | * File name and path of log file. 38 | * @var string 39 | */ 40 | private string $logFile; 41 | 42 | /** 43 | * Log channel--namespace for log lines. 44 | * Used to identify and correlate groups of similar log lines. 45 | * @var string 46 | */ 47 | private string $channel; 48 | 49 | /** 50 | * Lowest log level to log. 51 | * @var int 52 | */ 53 | private int $logLevel; 54 | 55 | /** 56 | * Whether to log to standard out. 57 | * @var bool 58 | */ 59 | private bool $stdout; 60 | 61 | /** 62 | * Log fields separated by tabs to form a TSV (CSV with tabs). 63 | */ 64 | private const TAB = "\t"; 65 | 66 | /** 67 | * Special minimum log level which will not log any log levels. 68 | */ 69 | public const LOG_LEVEL_NONE = 'none'; 70 | 71 | /** 72 | * Log level hierarchy 73 | */ 74 | public const LEVELS = [ 75 | self::LOG_LEVEL_NONE => -1, 76 | LogLevel::DEBUG => 0, 77 | LogLevel::INFO => 1, 78 | LogLevel::NOTICE => 2, 79 | LogLevel::WARNING => 3, 80 | LogLevel::ERROR => 4, 81 | LogLevel::CRITICAL => 5, 82 | LogLevel::ALERT => 6, 83 | LogLevel::EMERGENCY => 7, 84 | ]; 85 | 86 | /** 87 | * @param string $logFile File name and path of log file. 88 | * @param string $channel Logger channel associated with this logger. 89 | * @param string $logLevel (optional) Lowest log level to log. 90 | */ 91 | public function __construct(string $logFile, string $channel, string $logLevel = LogLevel::DEBUG) 92 | { 93 | $this->logFile = $logFile; 94 | $this->channel = $channel; 95 | $this->stdout = false; 96 | $this->setLogLevel($logLevel); 97 | } 98 | 99 | /** 100 | * Set the lowest log level to log. 101 | * 102 | * @param string $logLevel 103 | */ 104 | public function setLogLevel(string $logLevel): void 105 | { 106 | if (!\array_key_exists($logLevel, self::LEVELS)) { 107 | throw new \DomainException("Log level $logLevel is not a valid log level. Must be one of (" . \implode(', ', \array_keys(self::LEVELS)) . ')'); 108 | } 109 | 110 | $this->logLevel = self::LEVELS[$logLevel]; 111 | } 112 | 113 | /** 114 | * Set the log channel which identifies the log line. 115 | * 116 | * @param string $channel 117 | */ 118 | public function setChannel(string $channel): void 119 | { 120 | $this->channel = $channel; 121 | } 122 | 123 | /** 124 | * Set the standard out option on or off. 125 | * If set to true, log lines will also be printed to standard out. 126 | * 127 | * @param bool $stdout 128 | */ 129 | public function setOutput(bool $stdout): void 130 | { 131 | $this->stdout = $stdout; 132 | } 133 | 134 | /** 135 | * Log a debug message. 136 | * Fine-grained informational events that are most useful to debug an application. 137 | * 138 | * @param string|\Stringable $message Content of log event. 139 | * @param mixed[] $context Associative array of contextual support data that goes with the log event. 140 | * 141 | * @throws \RuntimeException 142 | */ 143 | public function debug(string|\Stringable $message = '', array $context = []): void 144 | { 145 | if ($this->logAtThisLevel(LogLevel::DEBUG)) { 146 | $this->log(LogLevel::DEBUG, $message, $context); 147 | } 148 | } 149 | 150 | /** 151 | * Log an info message. 152 | * Interesting events and informational messages that highlight the progress of the application at coarse-grained level. 153 | * 154 | * @param string|\Stringable $message Content of log event. 155 | * @param mixed[] $context Associative array of contextual support data that goes with the log event. 156 | * 157 | * @throws \RuntimeException 158 | */ 159 | public function info(string|\Stringable $message = '', array $context = []): void 160 | { 161 | if ($this->logAtThisLevel(LogLevel::INFO)) { 162 | $this->log(LogLevel::INFO, $message, $context); 163 | } 164 | } 165 | 166 | /** 167 | * Log an notice message. 168 | * Normal but significant events. 169 | * 170 | * @param string|\Stringable $message Content of log event. 171 | * @param mixed[] $context Associative array of contextual support data that goes with the log event. 172 | * 173 | * @throws \RuntimeException 174 | */ 175 | public function notice(string|\Stringable $message = '', array $context = []): void 176 | { 177 | if ($this->logAtThisLevel(LogLevel::NOTICE)) { 178 | $this->log(LogLevel::NOTICE, $message, $context); 179 | } 180 | } 181 | 182 | /** 183 | * Log a warning message. 184 | * Exceptional occurrences that are not errors--undesirable things that are not necessarily wrong. 185 | * Potentially harmful situations which still allow the application to continue running. 186 | * 187 | * @param string|\Stringable $message Content of log event. 188 | * @param mixed[] $context Associative array of contextual support data that goes with the log event. 189 | * 190 | * @throws \RuntimeException 191 | */ 192 | public function warning(string|\Stringable $message = '', array $context = []): void 193 | { 194 | if ($this->logAtThisLevel(LogLevel::WARNING)) { 195 | $this->log(LogLevel::WARNING, $message, $context); 196 | } 197 | } 198 | 199 | /** 200 | * Log an error message. 201 | * Error events that might still allow the application to continue running. 202 | * Runtime errors that do not require immediate action but should typically be logged and monitored. 203 | * 204 | * @param string|\Stringable $message Content of log event. 205 | * @param mixed[] $context Associative array of contextual support data that goes with the log event. 206 | * 207 | * @throws \RuntimeException 208 | */ 209 | public function error(string|\Stringable $message = '', array $context = []): void 210 | { 211 | if ($this->logAtThisLevel(LogLevel::ERROR)) { 212 | $this->log(LogLevel::ERROR, $message, $context); 213 | } 214 | } 215 | 216 | /** 217 | * Log a critical condition. 218 | * Application components being unavailable, unexpected exceptions, etc. 219 | * 220 | * @param string|\Stringable $message Content of log event. 221 | * @param mixed[] $context Associative array of contextual support data that goes with the log event. 222 | * 223 | * @throws \RuntimeException 224 | */ 225 | public function critical(string|\Stringable $message = '', array $context = []): void 226 | { 227 | if ($this->logAtThisLevel(LogLevel::CRITICAL)) { 228 | $this->log(LogLevel::CRITICAL, $message, $context); 229 | } 230 | } 231 | 232 | /** 233 | * Log an alert. 234 | * This should trigger an email or SMS alert and wake you up. 235 | * Example: Entire site down, database unavailable, etc. 236 | * 237 | * @param string|\Stringable $message Content of log event. 238 | * @param mixed[] $context Associative array of contextual support data that goes with the log event. 239 | * 240 | * @throws \RuntimeException 241 | */ 242 | public function alert(string|\Stringable $message = '', array $context = []): void 243 | { 244 | if ($this->logAtThisLevel(LogLevel::ALERT)) { 245 | $this->log(LogLevel::ALERT, $message, $context); 246 | } 247 | } 248 | 249 | /** 250 | * Log an emergency. 251 | * System is unusable. 252 | * This should trigger an email or SMS alert and wake you up. 253 | * 254 | * @param string|\Stringable $message Content of log event. 255 | * @param mixed[] $context Associative array of contextual support data that goes with the log event. 256 | * 257 | * @throws \RuntimeException 258 | */ 259 | public function emergency(string|\Stringable $message = '', array $context = []): void 260 | { 261 | if ($this->logAtThisLevel(LogLevel::EMERGENCY)) { 262 | $this->log(LogLevel::EMERGENCY, $message, $context); 263 | } 264 | } 265 | 266 | /** 267 | * Log a message. 268 | * Generic log routine that all severity levels use to log an event. 269 | * 270 | * @param mixed $level Log level 271 | * @param string|\Stringable $message Content of log event. 272 | * @param mixed[] $context Associative array of contextual support data that goes with the log event. 273 | * 274 | * @throws \RuntimeException when log file cannot be opened for writing. 275 | */ 276 | public function log($level, string|\Stringable $message = '', array $context = []): void 277 | { 278 | /** @var string $level */ 279 | 280 | // Build log line 281 | $pid = \getmypid() ?: -1; 282 | /** @var string $exception */ 283 | [$exception, $data] = $this->handleException($context); 284 | $data = $data ? \json_encode($data, \JSON_UNESCAPED_SLASHES) : '{}'; 285 | $data = $data ?: '{}'; // Fail-safe in case json_encode fails. 286 | $logLine = $this->formatLogLine($level, $pid, $message, $data, $exception); 287 | 288 | // Log to file 289 | try { 290 | $fh = \fopen($this->logFile, 'a'); 291 | if ($fh === false) { 292 | throw new \RuntimeException('fopen failed'); 293 | } 294 | \fwrite($fh, $logLine); 295 | \fclose($fh); 296 | } catch (\Throwable $e) { 297 | throw new \RuntimeException("Could not open log file {$this->logFile} for writing to SimpleLog channel {$this->channel}!", 0, $e); 298 | } 299 | 300 | // Log to stdout if option set to do so. 301 | if ($this->stdout) { 302 | print($logLine); 303 | } 304 | } 305 | 306 | /** 307 | * Determine if the logger should log at a certain log level. 308 | * 309 | * @param string $level 310 | * 311 | * @return bool True if we log at this level; false otherwise. 312 | */ 313 | private function logAtThisLevel(string $level): bool 314 | { 315 | return self::LEVELS[$level] >= $this->logLevel; 316 | } 317 | 318 | /** 319 | * Handle an exception in the data context array. 320 | * If an exception is included in the data context array, extract it. 321 | * 322 | * @param mixed[]|null $context 323 | * 324 | * @return mixed[] [exception, data (without exception)] 325 | */ 326 | private function handleException(array $context = null): array 327 | { 328 | if (isset($context['exception']) && $context['exception'] instanceof \Throwable) { 329 | $exception = $context['exception']; 330 | $exception_data = $this->buildExceptionData($exception); 331 | unset($context['exception']); 332 | } else { 333 | $exception_data = '{}'; 334 | } 335 | 336 | return [$exception_data, $context]; 337 | } 338 | 339 | /** 340 | * Build the exception log data. 341 | * 342 | * @param \Throwable $e 343 | * 344 | * @return string JSON {message, code, file, line, trace} 345 | */ 346 | private function buildExceptionData(\Throwable $e): string 347 | { 348 | $exceptionData = \json_encode( 349 | [ 350 | 'message' => $e->getMessage(), 351 | 'code' => $e->getCode(), 352 | 'file' => $e->getFile(), 353 | 'line' => $e->getLine(), 354 | 'trace' => $e->getTrace() 355 | ], 356 | \JSON_UNESCAPED_SLASHES 357 | ); 358 | 359 | // Fail-safe in case json_encode failed 360 | return $exceptionData ?: '{"message":"' . $e->getMessage() . '"}'; 361 | } 362 | 363 | /** 364 | * Format the log line. 365 | * YYYY-mm-dd HH:ii:ss.uuuuuu [loglevel] [channel] [pid:##] Log message content {"Optional":"JSON Contextual Support Data"} {"Optional":"Exception Data"} 366 | * 367 | * @param string $level 368 | * @param int $pid 369 | * @param string $message 370 | * @param string $data 371 | * @param string $exceptionData 372 | * 373 | * @return string 374 | */ 375 | private function formatLogLine(string $level, int $pid, string $message, string $data, string $exceptionData): string 376 | { 377 | return 378 | $this->getTime() . self::TAB . 379 | "[$level]" . self::TAB . 380 | "[{$this->channel}]" . self::TAB . 381 | "[pid:$pid]" . self::TAB . 382 | \str_replace(\PHP_EOL, ' ', trim($message)) . self::TAB . 383 | \str_replace(\PHP_EOL, ' ', $data) . self::TAB . 384 | \str_replace(\PHP_EOL, ' ', $exceptionData) . \PHP_EOL; 385 | } 386 | 387 | /** 388 | * Get current date time, with microsecond precision. 389 | * Format: YYYY-mm-dd HH:ii:ss.uuuuuu 390 | * 391 | * @return string Date time 392 | */ 393 | private function getTime(): string 394 | { 395 | return (new \DateTimeImmutable('now'))->format('Y-m-d H:i:s.u'); 396 | } 397 | } 398 | -------------------------------------------------------------------------------- /tests/Fixture/StringableMessage.php: -------------------------------------------------------------------------------- 1 | message = $message; 12 | } 13 | 14 | public function __toString() 15 | { 16 | return $this->message; 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /tests/LoggerTest.php: -------------------------------------------------------------------------------- 1 | logFile = tempnam('/tmp', 'SimpleLogUnitTest'); 42 | 43 | if (\file_exists($this->logFile)) { 44 | \unlink($this->logFile); 45 | } 46 | $this->logger = new Logger($this->logFile, self::TEST_CHANNEL); 47 | } 48 | 49 | /** 50 | * Clean up test by removing temporary log file. 51 | */ 52 | public function tearDown(): void 53 | { 54 | if (file_exists($this->logFile)) { 55 | unlink($this->logFile); 56 | } 57 | } 58 | 59 | /** 60 | * @test Logger implements PSR-3 Psr\Log\LoggerInterface 61 | */ 62 | public function testLoggerImplementsPRS3Interface() 63 | { 64 | $this->assertInstanceOf(\Psr\Log\LoggerInterface::class, $this->logger); 65 | } 66 | 67 | /** 68 | * @test Constructor sets expected properties. 69 | * @throws \Exception 70 | */ 71 | public function testConstructorSetsProperties() 72 | { 73 | // Given 74 | $logFileProperty = new \ReflectionProperty(Logger::class, 'logFile'); 75 | $channelProperty = new \ReflectionProperty(Logger::class, 'channel'); 76 | $stdoutProperty = new \ReflectionProperty(Logger::class, 'stdout'); 77 | $logLevelProperty = new \ReflectionProperty(Logger::class, 'logLevel'); 78 | 79 | // And 80 | $logFileProperty->setAccessible(true); 81 | $channelProperty->setAccessible(true); 82 | $stdoutProperty->setAccessible(true); 83 | $logLevelProperty->setAccessible(true); 84 | 85 | // Then 86 | $this->assertEquals($this->logFile, $logFileProperty->getValue($this->logger)); 87 | $this->assertEquals(self::TEST_CHANNEL, $channelProperty->getValue($this->logger)); 88 | $this->assertFalse($stdoutProperty->getValue($this->logger)); 89 | $this->assertEquals(Logger::LEVELS[LogLevel::DEBUG], $logLevelProperty->getValue($this->logger)); 90 | } 91 | 92 | /** 93 | * @test setLogLevel sets the correct log level. 94 | * @dataProvider dataProviderForSetLogLevel 95 | * @param string $logLevel 96 | * @param int $expectedLogLevelCode 97 | * @throws \Exception 98 | */ 99 | public function testSetLogLevelUsingConstants(string $logLevel, int $expectedLogLevelCode) 100 | { 101 | // Given 102 | $this->logger->setLogLevel($logLevel); 103 | $logLevelProperty = new \ReflectionProperty(Logger::class, 'logLevel'); 104 | $logLevelProperty->setAccessible(true); 105 | 106 | // When 107 | $logLevelCode = $logLevelProperty->getValue($this->logger); 108 | 109 | // Then 110 | $this->assertEquals($expectedLogLevelCode, $logLevelCode); 111 | } 112 | 113 | /** 114 | * @return array [log level, log level code] 115 | */ 116 | public function dataProviderForSetLogLevel(): array 117 | { 118 | return [ 119 | [Logger::LOG_LEVEL_NONE, Logger::LEVELS[Logger::LOG_LEVEL_NONE]], 120 | [LogLevel::DEBUG, Logger::LEVELS[LogLevel::DEBUG]], 121 | [LogLevel::INFO, Logger::LEVELS[LogLevel::INFO]], 122 | [LogLevel::NOTICE, Logger::LEVELS[LogLevel::NOTICE]], 123 | [LogLevel::WARNING, Logger::LEVELS[LogLevel::WARNING]], 124 | [LogLevel::ERROR, Logger::LEVELS[LogLevel::ERROR]], 125 | [LogLevel::CRITICAL, Logger::LEVELS[LogLevel::CRITICAL]], 126 | [LogLevel::ALERT, Logger::LEVELS[LogLevel::ALERT]], 127 | [LogLevel::EMERGENCY, Logger::LEVELS[LogLevel::EMERGENCY]], 128 | ]; 129 | } 130 | 131 | /** 132 | * @test setLogLevel throws a \DomainException when set to an invalid log level. 133 | * @throws \Exception 134 | */ 135 | public function testSetLogLevelWithBadLevelException() 136 | { 137 | // Then 138 | $this->expectException(\DomainException::class); 139 | 140 | // When 141 | $this->logger->setLogLevel('ThisLogLevelDoesNotExist'); 142 | } 143 | 144 | 145 | /** 146 | * @test setChannel sets the channel property. 147 | * @dataProvider dataProviderForSetChannel 148 | * @param string $channel 149 | * @throws \Exception 150 | */ 151 | public function testSetChannel(string $channel) 152 | { 153 | // Given 154 | $channelProperty = new \ReflectionProperty(Logger::class, 'channel'); 155 | $channelProperty->setAccessible(true); 156 | 157 | // When 158 | $this->logger->setChannel($channel); 159 | 160 | // Then 161 | $this->assertEquals($channel, $channelProperty->getValue($this->logger)); 162 | } 163 | 164 | /** 165 | * @return array [channel] 166 | */ 167 | public function dataProviderForSetChannel(): array 168 | { 169 | return [ 170 | ['newchannel'], 171 | ['evennewerchannel'], 172 | ]; 173 | } 174 | 175 | /** 176 | * @test setOutput sets the stdout property. 177 | * @dataProvider dataProviderForSetOutput 178 | * @param bool $output 179 | * @throws \Exception 180 | */ 181 | public function testSetOutput(bool $output) 182 | { 183 | // Given 184 | $stdout_property = new \ReflectionProperty(Logger::class, 'stdout'); 185 | $stdout_property->setAccessible(true); 186 | 187 | // When 188 | $this->logger->setOutput($output); 189 | 190 | // Then 191 | $this->assertEquals($output, $stdout_property->getValue($this->logger)); 192 | } 193 | 194 | /** 195 | * @return array [output] 196 | */ 197 | public function dataProviderForSetOutput(): array 198 | { 199 | return [ 200 | [true], 201 | [false], 202 | ]; 203 | } 204 | 205 | /** 206 | * @test Logger creates properly formatted log lines with the right log level for a string. 207 | * @dataProvider dataProviderForLogging 208 | * @param string $logLevel 209 | */ 210 | public function testLoggingWithString(string $logLevel) 211 | { 212 | // When 213 | $this->logger->$logLevel(self::TEST_MESSAGE); 214 | $logLine = \trim(\file_get_contents($this->logFile)); 215 | 216 | // Then 217 | $this->assertTrue((bool) preg_match(self::TEST_LOG_REGEX, $logLine)); 218 | $this->assertTrue((bool) preg_match("/\[$logLevel\]/", $logLine)); 219 | } 220 | 221 | /** 222 | * @test Logger creates properly formatted log lines with the right log level for a Stringable. 223 | * @dataProvider dataProviderForLogging 224 | * @param string $logLevel 225 | */ 226 | public function testLoggingWithStringable(string $logLevel) 227 | { 228 | // Given 229 | $message = new StringableMessage(self::TEST_MESSAGE); 230 | 231 | // When 232 | $this->logger->$logLevel($message); 233 | $logLine = \trim(\file_get_contents($this->logFile)); 234 | 235 | // Then 236 | $this->assertTrue((bool) preg_match(self::TEST_LOG_REGEX, $logLine)); 237 | $this->assertTrue((bool) preg_match("/\[$logLevel\]/", $logLine)); 238 | } 239 | 240 | /** 241 | * @return array [loglevel] 242 | */ 243 | public function dataProviderForLogging(): array 244 | { 245 | return [ 246 | ['debug'], 247 | ['info'], 248 | ['notice'], 249 | ['warning'], 250 | ['error'], 251 | ['critical'], 252 | ['alert'], 253 | ['emergency'], 254 | ]; 255 | } 256 | 257 | /** 258 | * @test Data context array shows up as a JSON string. 259 | */ 260 | public function testDataContext() 261 | { 262 | // When 263 | $this->logger->info(self::TEST_MESSAGE, ['key1' => 'value1', 'key2' => 6]); 264 | $logLine = \trim(\file_get_contents($this->logFile)); 265 | 266 | // Then 267 | $this->assertTrue((bool) \preg_match('/\s{"key1":"value1","key2":6}\s/', $logLine)); 268 | } 269 | 270 | /** 271 | * @test Logging an exception 272 | */ 273 | public function testExceptionTextWhenLoggingErrorWithExceptionData() 274 | { 275 | // Given 276 | $e = new \Exception('Exception123'); 277 | 278 | // When 279 | $this->logger->error('Testing the Exception', ['exception' => $e]); 280 | $logLine = \trim(\file_get_contents($this->logFile)); 281 | 282 | // Then 283 | $this->assertTrue((bool) \preg_match('/Testing the Exception/', $logLine)); 284 | $this->assertTrue((bool) \preg_match('/Exception123/', $logLine)); 285 | $this->assertTrue((bool) \preg_match('/code/', $logLine)); 286 | $this->assertTrue((bool) \preg_match('/file/', $logLine)); 287 | $this->assertTrue((bool) \preg_match('/line/', $logLine)); 288 | $this->assertTrue((bool) \preg_match('/trace/', $logLine)); 289 | } 290 | 291 | /** 292 | * @test Log lines will be on a single line even if there are newline characters in the log message. 293 | */ 294 | public function testLogMessageIsOneLineEvenThoughItHasNewLineCharacters() 295 | { 296 | // When 297 | $this->logger->info("This message has a new line\nAnd another\n", ['key' => 'value']); 298 | 299 | // Then 300 | $logLines = \file($this->logFile); 301 | $this->assertEquals(1, \count($logLines)); 302 | } 303 | 304 | /** 305 | * @test Log lines will be on a single line even if there are newline characters in the log message. 306 | */ 307 | public function testLogMessageIsOneLineEvenThoughItHasNewLineCharactersInData() 308 | { 309 | // When 310 | $this->logger->info('Log message', ['key' => "Value\nwith\new\lines\n"]); 311 | 312 | // Then 313 | $logLines = \file($this->logFile); 314 | $this->assertEquals(1, \count($logLines)); 315 | } 316 | 317 | /** 318 | * @test Log lines will be on a single line even if there are newline characters in the exception. 319 | */ 320 | public function testLogMessageIsOneLineEvenThoughItHasNewLineCharactersInException() 321 | { 322 | // When 323 | $this->logger->info('Log message', ['key' => 'value', 'exception' => new \Exception("This\nhas\newlines\nin\nit")]); 324 | 325 | // Then 326 | $logLines = \file($this->logFile); 327 | $this->assertEquals(1, \count($logLines)); 328 | } 329 | 330 | /** 331 | * @test Minimum log levels determine what log levels get logged. 332 | */ 333 | public function testMinimumLogLevels() 334 | { 335 | // When 336 | $this->logger->setLogLevel(LogLevel::ERROR); 337 | 338 | // When 339 | $this->logger->debug('This will not be logged.'); 340 | $this->logger->info('This will not be logged.'); 341 | $this->logger->notice('This will not be logged.'); 342 | $this->logger->warning('This will not be logged.'); 343 | 344 | // And 345 | $this->logger->error('This will be logged.'); 346 | $this->logger->critical('This will be logged.'); 347 | $this->logger->alert('This will be logged.'); 348 | $this->logger->emergency('This will be logged.'); 349 | 350 | // Then 351 | $logLines = \file($this->logFile); 352 | $this->assertEquals(4, \count($logLines)); 353 | } 354 | 355 | /** 356 | * @test Minimum log levels determine what log levels get logged. 357 | */ 358 | public function testMinimumLogLevelsByCheckingFileExistsBelowLogLevel() 359 | { 360 | // Given 361 | $this->logger->setLogLevel(LogLevel::ERROR); 362 | 363 | // When 364 | $this->logger->debug('This will not be logged.'); 365 | $this->logger->info('This will not be logged.'); 366 | $this->logger->notice('This will not be logged.'); 367 | $this->logger->warning('This will not be logged.'); 368 | 369 | // Then 370 | $this->assertFalse(\file_exists($this->logFile)); 371 | 372 | $this->logger->error('This will be logged.'); 373 | $this->assertTrue(\file_exists($this->logFile)); 374 | } 375 | 376 | /** 377 | * @test Minimum log levels determine what log levels get logged. 378 | */ 379 | public function testMinimumLogLevelsByCheckingFileExistsAboveLogLevel() 380 | { 381 | // Given 382 | $this->logger->setLogLevel(LogLevel::ERROR); 383 | 384 | // When 385 | $this->logger->error('This will be logged.'); 386 | 387 | // Then 388 | $this->assertTrue(\file_exists($this->logFile)); 389 | } 390 | 391 | /** 392 | * @test Exception is thrown if the log file cannot be opened. 393 | * @throws \Exception 394 | */ 395 | public function testLogExceptionCannotOpenFile() 396 | { 397 | // Given 398 | $badLogger = new Logger('/this/file/should/not/exist/on/any/system/if/it/does/well/oh/well/this/test/will/fail/logfile123.loglog.log', self::TEST_CHANNEL); 399 | 400 | // Then 401 | $this->expectException(\RuntimeException::class); 402 | 403 | // When 404 | $badLogger->info('This is not going to work, hence the test for the exception!'); 405 | } 406 | 407 | /** 408 | * @test After setting output to true the logger will output log lines to STDOUT. 409 | */ 410 | public function testLoggingToStdOut() 411 | { 412 | // Given 413 | $this->logger->setOutput(true); 414 | 415 | // Then 416 | $this->expectOutputRegex('/^\d{4}-\d{2}-\d{2} [ ] \d{2}:\d{2}:\d{2}[.]\d{6} \s \[\w+\] \s \[\w+\] \s \[pid:\d+\] \s Test Message \s {.*} \s {.*}/x'); 417 | 418 | // When 419 | $this->logger->info('TestMessage'); 420 | } 421 | 422 | /** 423 | * @test Time should be in YYYY-MM-DD HH:mm:SS.uuuuuu format. 424 | * @throws \Exception 425 | */ 426 | public function testGetTime() 427 | { 428 | // Given 429 | $reflection = new \ReflectionClass($this->logger); 430 | $method = $reflection->getMethod('getTime'); 431 | $method->setAccessible(true); 432 | 433 | // When 434 | $time = $method->invoke($this->logger); 435 | 436 | // Then 437 | $this->assertMatchesRegularExpression('/^\d{4}-\d{2}-\d{2} \d{2}:\d{2}:\d{2}[.]\d{6}$/', $time); 438 | } 439 | } 440 | -------------------------------------------------------------------------------- /tests/bootstrap.php: -------------------------------------------------------------------------------- 1 | 2 | 3 | The coding standard for SimpleLog. 4 | 5 | 6 | 7 | 8 | 9 | 10 | -------------------------------------------------------------------------------- /tests/phpstan.neon: -------------------------------------------------------------------------------- 1 | parameters: 2 | level: max 3 | paths: 4 | - ../src 5 | treatPhpDocTypesAsCertain: false 6 | -------------------------------------------------------------------------------- /tests/phpunit.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | ../src/ 6 | 7 | 8 | ../vendor/ 9 | 10 | 11 | 12 | 13 | . 14 | 15 | 16 | 17 | -------------------------------------------------------------------------------- /tests/psalm.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | --------------------------------------------------------------------------------