├── .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 | 
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 | [](https://coveralls.io/github/markrogoyski/simplelog-php?branch=master)
15 | [](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 | [](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 |
--------------------------------------------------------------------------------