├── .phpunit-watcher.yml
├── .styleci.yml
├── CHANGELOG.md
├── LICENSE.md
├── README.md
├── composer.json
├── config
├── events-console.php
└── events-web.php
├── infection.json.dist
├── psalm.xml
├── rector.php
└── src
├── ContextProvider
├── CommonContextProvider.php
├── CompositeContextProvider.php
├── ContextProviderInterface.php
└── SystemContextProvider.php
├── Logger.php
├── Message.php
├── Message
├── CategoryFilter.php
├── ContextValueExtractor.php
└── Formatter.php
├── PsrTarget.php
├── StreamTarget.php
└── Target.php
/.phpunit-watcher.yml:
--------------------------------------------------------------------------------
1 | watch:
2 | directories:
3 | - src
4 | - tests
5 | fileMask: '*.php'
6 | notifications:
7 | passingTests: false
8 | failingTests: false
9 | phpunit:
10 | binaryPath: vendor/bin/phpunit
11 | timeout: 180
12 |
--------------------------------------------------------------------------------
/.styleci.yml:
--------------------------------------------------------------------------------
1 | preset: psr12
2 | risky: true
3 |
4 | version: 8.1
5 |
6 | finder:
7 | exclude:
8 | - docs
9 | - vendor
10 |
11 | enabled:
12 | - alpha_ordered_traits
13 | - array_indentation
14 | - array_push
15 | - combine_consecutive_issets
16 | - combine_consecutive_unsets
17 | - combine_nested_dirname
18 | - declare_strict_types
19 | - dir_constant
20 | - fully_qualified_strict_types
21 | - function_to_constant
22 | - hash_to_slash_comment
23 | - is_null
24 | - logical_operators
25 | - magic_constant_casing
26 | - magic_method_casing
27 | - method_separation
28 | - modernize_types_casting
29 | - native_function_casing
30 | - native_function_type_declaration_casing
31 | - no_alias_functions
32 | - no_empty_comment
33 | - no_empty_phpdoc
34 | - no_empty_statement
35 | - no_extra_block_blank_lines
36 | - no_short_bool_cast
37 | - no_superfluous_elseif
38 | - no_unneeded_control_parentheses
39 | - no_unneeded_curly_braces
40 | - no_unneeded_final_method
41 | - no_unset_cast
42 | - no_unused_imports
43 | - no_unused_lambda_imports
44 | - no_useless_else
45 | - no_useless_return
46 | - normalize_index_brace
47 | - php_unit_dedicate_assert
48 | - php_unit_dedicate_assert_internal_type
49 | - php_unit_expectation
50 | - php_unit_mock
51 | - php_unit_mock_short_will_return
52 | - php_unit_namespaced
53 | - php_unit_no_expectation_annotation
54 | - phpdoc_no_empty_return
55 | - phpdoc_no_useless_inheritdoc
56 | - phpdoc_order
57 | - phpdoc_property
58 | - phpdoc_scalar
59 | - phpdoc_singular_inheritdoc
60 | - phpdoc_trim
61 | - phpdoc_trim_consecutive_blank_line_separation
62 | - phpdoc_type_to_var
63 | - phpdoc_types
64 | - phpdoc_types_order
65 | - print_to_echo
66 | - regular_callable_call
67 | - return_assignment
68 | - self_accessor
69 | - self_static_accessor
70 | - set_type_to_cast
71 | - short_array_syntax
72 | - short_list_syntax
73 | - simplified_if_return
74 | - single_quote
75 | - standardize_not_equals
76 | - ternary_to_null_coalescing
77 | - trailing_comma_in_multiline_array
78 | - unalign_double_arrow
79 | - unalign_equals
80 | - empty_loop_body_braces
81 | - integer_literal_case
82 | - union_type_without_spaces
83 |
84 | disabled:
85 | - function_declaration
86 |
--------------------------------------------------------------------------------
/CHANGELOG.md:
--------------------------------------------------------------------------------
1 | # Yii Logging Library Change Log
2 |
3 | ## 2.1.2 under development
4 |
5 | - no changes in this release.
6 |
7 | ## 2.1.1 June 03, 2025
8 |
9 | - Enh #123, #124: Minor refactor internal class `ContextValueExtractor` (@Tigrov, @vjik)
10 | - Bug #123: Explicitly marking parameters as nullable (@Tigrov)
11 |
12 | ## 2.1.0 July 03, 2024
13 |
14 | - New #104: Add new static methods `Logger::assertLevelIsValid()`, `Logger::assertLevelIsString()` and
15 | `Logger::assertLevelIsSupported()` (@vjik)
16 | - New #108: Support of nested values in message templates' variables, e. g. `{foo.bar}` (@vjik)
17 | - New #109, #113, #116: Add context providers (@vjik)
18 | - New #111: Add `DateTime` and `DateTimeImmutable` support as time in log context (@vjik)
19 | - New #112: Add `Message::category()` method and `Message::DEFAULT_CATEGORY` constant, deprecate
20 | `CategoryFilter::DEFAULT` in favor it (@vjik)
21 | - New #113: Add `Message::trace()` method (@vjik)
22 | - New #114: Add `Message::time()` method (@vjik)
23 | - Chg #104: Deprecate method `Logger::validateLevel()` (@vjik)
24 | - Chg #109: Deprecate `Logger` methods `setTraceLevel()` and `setExcludedTracePaths()` in favor of context provider
25 | usage (@vjik)
26 | - Chg #116: Deprecate methods `setCommonContext()` and `getCommonContext()` in `Target` class (@vjik)
27 | - Chg #118: Replace `gettype()` to `get_debug_type()` in exception messages generation (@vjik)
28 | - Bug #84: Change the type of the `$level` parameter in the `Message` constructor to `string` (@dood-)
29 | - Bug #89: Fix error on parse messages, that contains variables that cannot cast to a string (@vjik)
30 | - Bug #98: Fix error on formatting trace, when it doesn't contain "file" and "line" (@vjik)
31 |
32 | ## 2.0.0 May 22, 2022
33 |
34 | - Chg #68: Raise the minimum `psr/log` version to `^2.0|^3.0` and the minimum PHP version to 8.0 (@xepozz, @rustamwin)
35 |
36 | ## 1.0.4 March 29, 2022
37 |
38 | - Bug #76: Fix time formatter when locale uses comma as a decimal point separator (@terabytesoftw)
39 |
40 | ## 1.0.3 November 12, 2021
41 |
42 | - Chg #74: Replace usage of `yiisoft/yii-web` to `yiisoft/yii-http` in event config (@devanych)
43 |
44 | ## 1.0.2 May 19, 2021
45 |
46 | - Bug #67: Flush logger on the console is terminated (@rustamwin)
47 |
48 | ## 1.0.1 March 23, 2021
49 |
50 | - Chg: Adjust config for new config plugin (@samdark)
51 |
52 | ## 1.0.0 February 11, 2021
53 |
54 | Initial release.
55 |
--------------------------------------------------------------------------------
/LICENSE.md:
--------------------------------------------------------------------------------
1 | Copyright © 2008 by Yii Software ()
2 | All rights reserved.
3 |
4 | Redistribution and use in source and binary forms, with or without
5 | modification, are permitted provided that the following conditions
6 | are met:
7 |
8 | * Redistributions of source code must retain the above copyright
9 | notice, this list of conditions and the following disclaimer.
10 | * Redistributions in binary form must reproduce the above copyright
11 | notice, this list of conditions and the following disclaimer in
12 | the documentation and/or other materials provided with the
13 | distribution.
14 | * Neither the name of Yii Software nor the names of its
15 | contributors may be used to endorse or promote products derived
16 | from this software without specific prior written permission.
17 |
18 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
19 | "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
20 | LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS
21 | FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE
22 | COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT,
23 | INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING,
24 | BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
25 | LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
26 | CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT
27 | LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN
28 | ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
29 | POSSIBILITY OF SUCH DAMAGE.
30 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
Yii Logging Library
6 |
7 |
8 |
9 | [](https://packagist.org/packages/yiisoft/log)
10 | [](https://packagist.org/packages/yiisoft/log)
11 | [](https://github.com/yiisoft/log/actions/workflows/build.yml)
12 | [](https://codecov.io/gh/yiisoft/log)
13 | [](https://dashboard.stryker-mutator.io/reports/github.com/yiisoft/log/master)
14 | [](https://github.com/yiisoft/log/actions?query=workflow%3A%22static+analysis%22)
15 | [](https://shepherd.dev/github/yiisoft/log)
16 |
17 | This package provides a [PSR-3](https://www.php-fig.org/psr/psr-3/) compatible logging library. It is used extensively in the
18 | [Yii Framework](https://www.yiiframework.com/) but it can also be used as a separate package.
19 |
20 | The logger sends or passes messages to multiple targets. Each target may filter these messages according to their severity level, and category, and then export them to some medium such as a file, an email or a syslog.
21 |
22 | ## Requirements
23 |
24 | - PHP 8.0 or higher.
25 |
26 | ## Installation
27 |
28 | The package can be installed with [Composer](https://getcomposer.org):
29 |
30 | ```shell
31 | composer require yiisoft/log
32 | ```
33 |
34 | ## General usage
35 |
36 | Creating a logger:
37 |
38 | ```php
39 | /**
40 | * List of class instances that extend the \Yiisoft\Log\Target abstract class.
41 | *
42 | * @var \Yiisoft\Log\Target[] $targets
43 | */
44 | $logger = new \Yiisoft\Log\Logger($targets);
45 | ```
46 |
47 | Writing logs:
48 |
49 | ```php
50 | $logger->emergency('Emergency message', ['key' => 'value']);
51 | $logger->alert('Alert message', ['key' => 'value']);
52 | $logger->critical('Critical message', ['key' => 'value']);
53 | $logger->warning('Warning message', ['key' => 'value']);
54 | $logger->notice('Notice message', ['key' => 'value']);
55 | $logger->info('Info message', ['key' => 'value']);
56 | $logger->debug('Debug message', ['key' => 'value']);
57 | ```
58 |
59 | ### Message Flushing and Exporting
60 |
61 | Log messages are collected and stored in memory. To limit memory consumption, the logger will flush
62 | the recorded messages to the log targets each time a certain number of log messages accumulate.
63 | You can customize this number by calling the `\Yiisoft\Log\Logger::setFlushInterval()` method:
64 |
65 | ```php
66 | $logger->setFlushInterval(100); // default is 1000
67 | ```
68 |
69 | Each log target also collects and stores messages in memory.
70 | Message exporting in a target follows the same principle as in the logger.
71 | To change the number of stored messages, call the `\Yiisoft\Log\Target::setExportInterval()` method:
72 |
73 | ```php
74 | $target->setExportInterval(100); // default is 1000
75 | ```
76 |
77 | > Note: All message flushing and exporting also occurs when the application ends.
78 |
79 | ### Logging targets
80 |
81 | This package contains two targets:
82 |
83 | - `Yiisoft\Log\PsrTarget` - passes log messages to another [PSR-3](https://www.php-fig.org/psr/psr-3/) compatible logger.
84 | - `Yiisoft\Log\StreamTarget` - writes log messages to the specified output stream.
85 |
86 | Extra logging targets are implemented as separate packages:
87 |
88 | - [Database](https://github.com/yiisoft/log-target-db)
89 | - [Email](https://github.com/yiisoft/log-target-email)
90 | - [File](https://github.com/yiisoft/log-target-file)
91 | - [Syslog](https://github.com/yiisoft/log-target-syslog)
92 |
93 | ### Context providers
94 |
95 | Context providers are used to provide additional context data for log messages. You can define your own context provider
96 | in the `Logger` constructor:
97 |
98 | ```php
99 | $logger = new \Yiisoft\Log\Logger(contextProvider: $myContextProvider);
100 | ```
101 |
102 | Out of the box, the following context providers are available:
103 |
104 | - `SystemContextProvider` — adds system information (time, memory usage, trace, default category);
105 | - `CommonContextProvider` — adds common data;
106 | - `CompositeContextProvider` — allows combining multiple context providers.
107 |
108 | By default, the logger uses the built-in `SystemContextProvider`.
109 |
110 | #### `SystemContextProvider`
111 |
112 | The `SystemContextProvider` adds the following data to the context:
113 |
114 | - `time` — current Unix timestamp with microseconds (float value);
115 | - `trace` — array of call stack information;
116 | - `memory` — memory usage in bytes.
117 | - `category` — category of the log message (always "application").
118 |
119 | `Yiisoft\Log\ContextProvider\SystemContextProvider` constructor parameters:
120 |
121 | - `traceLevel` — how much call stack information (file name and line number) should be logged for each
122 | log message. If the traceLevel is greater than 0, a similar number of call stacks will be logged at most. Note that only
123 | application call stacks are counted.
124 | - `excludedTracePaths` — array of paths to exclude from tracing when tracing is enabled with `traceLevel`.
125 |
126 | An example of custom parameters' usage:
127 |
128 | ```php
129 | $logger = new \Yiisoft\Log\Logger(
130 | contextProvider: new Yiisoft\Log\ContextProvider\SystemContextProvider(
131 | traceLevel: 3,
132 | excludedTracePaths: [
133 | '/vendor/yiisoft/di',
134 | ],
135 | ),
136 | );
137 | ```
138 |
139 | #### `CommonContextProvider`
140 |
141 | The `CommonContextProvider` allows the adding of additional common information to the log context, for example:
142 |
143 | ```php
144 | $logger = new \Yiisoft\Log\Logger(
145 | contextProvider: new Yiisoft\Log\ContextProvider\CommonContextProvider([
146 | 'environment' => 'production',
147 | ]),
148 | );
149 | ```
150 |
151 | #### `CompositeContextProvider`
152 |
153 | The `CompositeContextProvider` allows the combining of multiple context providers into one, for example:
154 |
155 | ```php
156 | $logger = new \Yiisoft\Log\Logger(
157 | contextProvider: new Yiisoft\Log\ContextProvider\CompositeContextProvider(
158 | new Yiisoft\Log\ContextProvider\SystemContextProvider(),
159 | new Yiisoft\Log\ContextProvider\CommonContextProvider(['environment' => 'production'])
160 | ),
161 | );
162 | ```
163 |
164 | ## Documentation
165 |
166 | - [Yii guide to logging](https://github.com/yiisoft/docs/blob/master/guide/en/runtime/logging.md)
167 | - [Internals](docs/internals.md)
168 |
169 | If you need help or have a question, the [Yii Forum](https://forum.yiiframework.com/c/yii-3-0/63) is available.
170 | You may also check out other [Yii Community Resources](https://www.yiiframework.com/community).
171 |
172 | ## License
173 |
174 | The Yii Logging Library is free software. It is released under the terms of the BSD License.
175 | Please see [`LICENSE`](./LICENSE.md) for more information.
176 |
177 | Maintained by [Yii Software](https://www.yiiframework.com/).
178 |
179 | ## Support the project
180 |
181 | [](https://opencollective.com/yiisoft)
182 |
183 | ## Follow updates
184 |
185 | [](https://www.yiiframework.com/)
186 | [](https://twitter.com/yiiframework)
187 | [](https://t.me/yii3en)
188 | [](https://www.facebook.com/groups/yiitalk)
189 | [](https://yiiframework.com/go/slack)
190 |
--------------------------------------------------------------------------------
/composer.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "yiisoft/log",
3 | "type": "library",
4 | "description": "Yii Logging Library",
5 | "keywords": [
6 | "yii",
7 | "framework",
8 | "log",
9 | "logger",
10 | "psr-3"
11 | ],
12 | "homepage": "https://www.yiiframework.com/",
13 | "license": "BSD-3-Clause",
14 | "support": {
15 | "issues": "https://github.com/yiisoft/log/issues?state=open",
16 | "source": "https://github.com/yiisoft/log",
17 | "forum": "https://www.yiiframework.com/forum/",
18 | "wiki": "https://www.yiiframework.com/wiki/",
19 | "irc": "ircs://irc.libera.chat:6697/yii",
20 | "chat": "https://t.me/yii3en"
21 | },
22 | "funding": [
23 | {
24 | "type": "opencollective",
25 | "url": "https://opencollective.com/yiisoft"
26 | },
27 | {
28 | "type": "github",
29 | "url": "https://github.com/sponsors/yiisoft"
30 | }
31 | ],
32 | "require": {
33 | "php": "^8.0",
34 | "psr/log": "^2.0 || ^3.0",
35 | "yiisoft/var-dumper": "^1.0"
36 | },
37 | "require-dev": {
38 | "maglnet/composer-require-checker": "^4.4",
39 | "phpunit/phpunit": "^9.6.23",
40 | "rector/rector": "^2.0.17",
41 | "roave/infection-static-analysis-plugin": "^1.25",
42 | "spatie/phpunit-watcher": "^1.23.6",
43 | "vimeo/psalm": "^4.30 || ^5.26.1 || ^6.12"
44 | },
45 | "provide": {
46 | "psr/log-implementation": "1.0.0"
47 | },
48 | "suggest": {
49 | "yiisoft/log-target-db": "Allows writing log messages to the database",
50 | "yiisoft/log-target-email": "Allows sending log messages by email",
51 | "yiisoft/log-target-file": "Allows writing log messages to the files",
52 | "yiisoft/log-target-syslog": "Allows writing log messages to the Syslog"
53 | },
54 | "autoload": {
55 | "psr-4": {
56 | "Yiisoft\\Log\\": "src"
57 | }
58 | },
59 | "autoload-dev": {
60 | "psr-4": {
61 | "Yiisoft\\Log\\Tests\\": "tests"
62 | }
63 | },
64 | "extra": {
65 | "config-plugin-options": {
66 | "source-directory": "config"
67 | },
68 | "config-plugin": {
69 | "events-console": "events-console.php",
70 | "events-web": "events-web.php"
71 | }
72 | },
73 | "config": {
74 | "sort-packages": true,
75 | "allow-plugins": {
76 | "infection/extension-installer": true,
77 | "composer/package-versions-deprecated": true
78 | }
79 | },
80 | "scripts": {
81 | "test": "phpunit --testdox --no-interaction",
82 | "test-watch": "phpunit-watcher watch"
83 | }
84 | }
85 |
--------------------------------------------------------------------------------
/config/events-console.php:
--------------------------------------------------------------------------------
1 | [
11 | static function (LoggerInterface $logger): void {
12 | if ($logger instanceof Logger) {
13 | $logger->flush(true);
14 | }
15 | },
16 | ],
17 | ];
18 |
--------------------------------------------------------------------------------
/config/events-web.php:
--------------------------------------------------------------------------------
1 | [
11 | static function (LoggerInterface $logger): void {
12 | if ($logger instanceof Logger) {
13 | $logger->flush(true);
14 | }
15 | },
16 | ],
17 | ];
18 |
--------------------------------------------------------------------------------
/infection.json.dist:
--------------------------------------------------------------------------------
1 | {
2 | "source": {
3 | "directories": [
4 | "src"
5 | ]
6 | },
7 | "logs": {
8 | "text": "php:\/\/stderr",
9 | "stryker": {
10 | "report": "master"
11 | }
12 | },
13 | "mutators": {
14 | "@default": true
15 | }
16 | }
17 |
--------------------------------------------------------------------------------
/psalm.xml:
--------------------------------------------------------------------------------
1 |
2 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
--------------------------------------------------------------------------------
/rector.php:
--------------------------------------------------------------------------------
1 | withPaths([
11 | __DIR__ . '/src',
12 | __DIR__ . '/tests',
13 | ])
14 | ->withPhpSets(php80: true)
15 | ->withRules([
16 | InlineConstructorDefaultToPropertyRector::class,
17 | ])
18 | ->withSkip([
19 | ClosureToArrowFunctionRector::class,
20 | ]);
21 |
--------------------------------------------------------------------------------
/src/ContextProvider/CommonContextProvider.php:
--------------------------------------------------------------------------------
1 | data;
20 | }
21 | }
22 |
--------------------------------------------------------------------------------
/src/ContextProvider/CompositeContextProvider.php:
--------------------------------------------------------------------------------
1 | providers = $providers;
21 | }
22 |
23 | public function getContext(): array
24 | {
25 | $contexts = [];
26 | foreach ($this->providers as $provider) {
27 | $contexts[] = $provider->getContext();
28 | }
29 | return array_merge(...$contexts);
30 | }
31 | }
32 |
--------------------------------------------------------------------------------
/src/ContextProvider/ContextProviderInterface.php:
--------------------------------------------------------------------------------
1 | setExcludedTracePaths($excludedTracePaths);
33 | }
34 |
35 | public function getContext(): array
36 | {
37 | /** @psalm-var list $trace */
38 | $trace = debug_backtrace(DEBUG_BACKTRACE_IGNORE_ARGS);
39 | array_shift($trace);
40 | return [
41 | 'time' => microtime(true),
42 | 'trace' => $this->collectTrace($trace),
43 | 'memory' => memory_get_usage(),
44 | 'category' => Message::DEFAULT_CATEGORY,
45 | ];
46 | }
47 |
48 | /**
49 | * Sets how much call stack information (file name and line number) should be logged for each log message.
50 | *
51 | * @param int $traceLevel The number of call stack information.
52 | *
53 | * @see self::$traceLevel
54 | *
55 | * @deprecated since 2.1.0, to be removed in 3.0.0. Use constructor parameter "traceLevel" instead.
56 | */
57 | public function setTraceLevel(int $traceLevel): self
58 | {
59 | $this->traceLevel = $traceLevel;
60 | return $this;
61 | }
62 |
63 | /**
64 | * Sets an array of paths to exclude from tracing when tracing is enabled with {@see self::$traceLevel}.
65 | *
66 | * @param string[] $excludedTracePaths The paths to exclude from tracing.
67 | *
68 | * @throws InvalidArgumentException for non-string values.
69 | *
70 | * @see self::$excludedTracePaths
71 | *
72 | * @deprecated since 2.1.0, to be removed in 3.0.0. Use constructor parameter "excludedTracePaths" instead.
73 | */
74 | public function setExcludedTracePaths(array $excludedTracePaths): self
75 | {
76 | foreach ($excludedTracePaths as $excludedTracePath) {
77 | /** @psalm-suppress DocblockTypeContradiction */
78 | if (!is_string($excludedTracePath)) {
79 | throw new InvalidArgumentException(
80 | sprintf(
81 | 'The trace path must be a string, %s received.',
82 | get_debug_type($excludedTracePath)
83 | )
84 | );
85 | }
86 | }
87 |
88 | $this->excludedTracePaths = $excludedTracePaths;
89 | return $this;
90 | }
91 |
92 | /**
93 | * Collects a trace when tracing is enabled with {@see Logger::setTraceLevel()}.
94 | *
95 | * @param array[] $backtrace The list of call stack information.
96 | * @psalm-param list $backtrace
97 | *
98 | * @return array[] Collected a list of call stack information.
99 | * @psalm-return list
100 | */
101 | private function collectTrace(array $backtrace): array
102 | {
103 | $traces = [];
104 |
105 | if ($this->traceLevel > 0) {
106 | $count = 0;
107 |
108 | foreach ($backtrace as $trace) {
109 | if (isset($trace['file'], $trace['line'])) {
110 | $excludedMatch = array_filter(
111 | $this->excludedTracePaths,
112 | static fn($path) => str_contains($trace['file'], $path)
113 | );
114 |
115 | if (empty($excludedMatch)) {
116 | $traces[] = $trace;
117 | if (++$count >= $this->traceLevel) {
118 | break;
119 | }
120 | }
121 | }
122 | }
123 | }
124 |
125 | return $traces;
126 | }
127 | }
128 |
--------------------------------------------------------------------------------
/src/Logger.php:
--------------------------------------------------------------------------------
1 | setTargets($targets);
89 | $this->contextProvider = $contextProvider ?? new SystemContextProvider();
90 |
91 | register_shutdown_function(function () {
92 | // make regular flush before other shutdown functions, which allows session data collection and so on
93 | $this->flush();
94 | // make sure log entries written by shutdown functions are also flushed
95 | // ensure "flush()" is called last when there are multiple shutdown functions
96 | register_shutdown_function([$this, 'flush'], true);
97 | });
98 | }
99 |
100 | /**
101 | * Returns the text display of the specified level.
102 | *
103 | * @param mixed $level The message level, e.g. {@see LogLevel::ERROR}, {@see LogLevel::WARNING}.
104 | *
105 | * @throws \Psr\Log\InvalidArgumentException for invalid log message level.
106 | *
107 | * @return string The text display of the level.
108 | * @deprecated since 2.1, to be removed in 3.0. Use {@see LogLevel::assertLevelIsValid()} instead.
109 | */
110 | public static function validateLevel(mixed $level): string
111 | {
112 | if (!is_string($level)) {
113 | throw new \Psr\Log\InvalidArgumentException(sprintf(
114 | 'The log message level must be a string, %s provided.',
115 | get_debug_type($level)
116 | ));
117 | }
118 |
119 | if (!in_array($level, self::LEVELS, true)) {
120 | throw new \Psr\Log\InvalidArgumentException(sprintf(
121 | 'Invalid log message level "%s" provided. The following values are supported: "%s".',
122 | $level,
123 | implode('", "', self::LEVELS)
124 | ));
125 | }
126 |
127 | return $level;
128 | }
129 |
130 | /**
131 | * @return Target[] The log targets. Each array element represents a single {@see \Yiisoft\Log\Target} instance.
132 | */
133 | public function getTargets(): array
134 | {
135 | return $this->targets;
136 | }
137 |
138 | public function log(mixed $level, string|Stringable $message, array $context = []): void
139 | {
140 | self::assertLevelIsString($level);
141 |
142 | $this->messages[] = new Message(
143 | $level,
144 | $message,
145 | array_merge($this->contextProvider->getContext(), $context),
146 | );
147 |
148 | if ($this->flushInterval > 0 && count($this->messages) >= $this->flushInterval) {
149 | $this->flush();
150 | }
151 | }
152 |
153 | /**
154 | * Flushes log messages from memory to targets.
155 | *
156 | * @param bool $final Whether this is a final call during a request.
157 | */
158 | public function flush(bool $final = false): void
159 | {
160 | $messages = $this->messages;
161 | // https://github.com/yiisoft/yii2/issues/5619
162 | // new messages could be logged while the existing ones are being handled by targets
163 | $this->messages = [];
164 |
165 | $this->dispatch($messages, $final);
166 | }
167 |
168 | /**
169 | * Sets how many log messages should be logged before they are flushed from memory and sent to targets.
170 | *
171 | * @param int $flushInterval The number of messages to accumulate before flushing.
172 | *
173 | * @see Logger::$flushInterval
174 | */
175 | public function setFlushInterval(int $flushInterval): self
176 | {
177 | $this->flushInterval = $flushInterval;
178 | return $this;
179 | }
180 |
181 | /**
182 | * Sets how much call stack information (file name and line number) should be logged for each log message.
183 | *
184 | * @param int $traceLevel The number of call stack information.
185 | *
186 | * @deprecated since 2.1, to be removed in 3.0 version. Use {@see self::$contextProvider}
187 | * and {@see SystemContextProvider::setTraceLevel()} instead.
188 | */
189 | public function setTraceLevel(int $traceLevel): self
190 | {
191 | if (!$this->contextProvider instanceof SystemContextProvider) {
192 | throw new RuntimeException(
193 | '"Logger::setTraceLevel()" is unavailable when using a custom context provider.'
194 | );
195 | }
196 | /** @psalm-suppress DeprecatedMethod */
197 | $this->contextProvider->setTraceLevel($traceLevel);
198 | return $this;
199 | }
200 |
201 | /**
202 | * Sets an array of paths to exclude from tracing when tracing is enabled with {@see Logger::setTraceLevel()}.
203 | *
204 | * @param string[] $excludedTracePaths The paths to exclude from tracing.
205 | *
206 | * @throws InvalidArgumentException for non-string values.
207 | *
208 | * @deprecated since 2.1, to be removed in 3.0 version. Use {@see self::$contextProvider}
209 | * and {@see SystemContextProvider::setExcludedTracePaths()} instead.
210 | */
211 | public function setExcludedTracePaths(array $excludedTracePaths): self
212 | {
213 | if (!$this->contextProvider instanceof SystemContextProvider) {
214 | throw new RuntimeException(
215 | '"Logger::setExcludedTracePaths()" is unavailable when using a custom context provider.'
216 | );
217 | }
218 | /** @psalm-suppress DeprecatedMethod */
219 | $this->contextProvider->setExcludedTracePaths($excludedTracePaths);
220 | return $this;
221 | }
222 |
223 | /**
224 | * Asserts that the log message level is valid.
225 | *
226 | * @param mixed $level The message level.
227 | *
228 | * @throws \Psr\Log\InvalidArgumentException When the log message level is not a string or is not supported.
229 | */
230 | public static function assertLevelIsValid(mixed $level): void
231 | {
232 | self::assertLevelIsString($level);
233 | self::assertLevelIsSupported($level);
234 | }
235 |
236 | /**
237 | * Asserts that the log message level is a string.
238 | *
239 | * @param mixed $level The message level.
240 | *
241 | * @throws \Psr\Log\InvalidArgumentException When the log message level is not a string.
242 | *
243 | * @psalm-assert string $level
244 | */
245 | public static function assertLevelIsString(mixed $level): void
246 | {
247 | if (is_string($level)) {
248 | return;
249 | }
250 |
251 | throw new \Psr\Log\InvalidArgumentException(
252 | sprintf('The log message level must be a string, %s provided.', get_debug_type($level))
253 | );
254 | }
255 |
256 | /**
257 | * Asserts that the log message level is supported.
258 | *
259 | * @param string $level The message level.
260 | *
261 | * @throws \Psr\Log\InvalidArgumentException When the log message level is not supported.
262 | */
263 | public static function assertLevelIsSupported(string $level): void
264 | {
265 | if (in_array($level, self::LEVELS, true)) {
266 | return;
267 | }
268 |
269 | throw new \Psr\Log\InvalidArgumentException(
270 | sprintf(
271 | 'Invalid log message level "%s" provided. The following values are supported: "%s".',
272 | $level,
273 | implode('", "', self::LEVELS)
274 | )
275 | );
276 | }
277 |
278 | /**
279 | * Sets a target to {@see Logger::$targets}.
280 | *
281 | * @param Target[] $targets The log targets. Each array element represents a single {@see \Yiisoft\Log\Target}
282 | * instance or the configuration for creating the log target instance.
283 | *
284 | * @throws InvalidArgumentException for non-instance Target.
285 | */
286 | private function setTargets(array $targets): void
287 | {
288 | foreach ($targets as $target) {
289 | if (!($target instanceof Target)) {
290 | throw new InvalidArgumentException('You must provide an instance of \Yiisoft\Log\Target.');
291 | }
292 | }
293 |
294 | $this->targets = $targets;
295 | }
296 |
297 | /**
298 | * Dispatches the logged messages to {@see Logger::$targets}.
299 | *
300 | * @param Message[] $messages The log messages.
301 | * @param bool $final Whether this method is called at the end of the current application.
302 | */
303 | private function dispatch(array $messages, bool $final): void
304 | {
305 | $targetErrors = [];
306 |
307 | foreach ($this->targets as $target) {
308 | if ($target->isEnabled()) {
309 | try {
310 | $target->collect($messages, $final);
311 | } catch (Throwable $e) {
312 | $target->disable();
313 | $targetErrors[] = new Message(
314 | LogLevel::WARNING,
315 | 'Unable to send log via ' . $target::class . ': ' . $e::class . ': ' . $e->getMessage(),
316 | ['time' => microtime(true), 'exception' => $e],
317 | );
318 | }
319 | }
320 | }
321 |
322 | if (!empty($targetErrors)) {
323 | $this->dispatch($targetErrors, true);
324 | }
325 | }
326 | }
327 |
--------------------------------------------------------------------------------
/src/Message.php:
--------------------------------------------------------------------------------
1 | level = $level;
78 | $this->message = $this->parse($message, $context);
79 | $this->context = $context;
80 | $this->defaultTime = new DateTimeImmutable();
81 | }
82 |
83 | /**
84 | * Gets a log message level.
85 | *
86 | * @return string Log message level.
87 | */
88 | public function level(): string
89 | {
90 | return $this->level;
91 | }
92 |
93 | /**
94 | * Gets a log message.
95 | *
96 | * @return string Log message.
97 | */
98 | public function message(): string
99 | {
100 | return $this->message;
101 | }
102 |
103 | /**
104 | * Returns a value of the context parameter for the specified name.
105 | *
106 | * If no name is specified, the entire context is returned.
107 | *
108 | * @param string|null $name The context parameter name.
109 | * @param mixed $default If the context parameter does not exist, the `$default` will be returned.
110 | *
111 | * @return mixed The context parameter value.
112 | */
113 | public function context(?string $name = null, mixed $default = null): mixed
114 | {
115 | if ($name === null) {
116 | return $this->context;
117 | }
118 |
119 | return $this->context[$name] ?? $default;
120 | }
121 |
122 | /**
123 | * Returns the log message category. {@see self::DEFAULT_CATEGORY} is returned if the category is not set.
124 | *
125 | * @return string The log message category.
126 | */
127 | public function category(): string
128 | {
129 | $category = $this->context['category'] ?? self::DEFAULT_CATEGORY;
130 | if (!is_string($category)) {
131 | throw new LogicException(
132 | 'Invalid category value in log context. Expected "string", got "' . get_debug_type($category) . '".'
133 | );
134 | }
135 | return $category;
136 | }
137 |
138 | /**
139 | * Returns the debug trace.
140 | *
141 | * @return array[]|null The debug trace or null if the trace is not set.
142 | *
143 | * @psalm-return list|null
144 | */
145 | public function trace(): ?array
146 | {
147 | $trace = $this->context['trace'] ?? null;
148 | if ($trace === null) {
149 | return null;
150 | }
151 |
152 | /**
153 | * @psalm-var list $trace We believe that the debug trace in context is always received as result of call
154 | * `debug_backtrace(DEBUG_BACKTRACE_IGNORE_ARGS)`.
155 | */
156 | return $trace;
157 | }
158 |
159 | /**
160 | * Returns the time of the log message.
161 | *
162 | * @return DateTimeImmutable The log message time.
163 | */
164 | public function time(): DateTimeImmutable
165 | {
166 | $time = $this->context['time'] ?? $this->defaultTime;
167 |
168 | if ($time instanceof DateTimeInterface) {
169 | return DateTimeImmutable::createFromInterface($time);
170 | }
171 |
172 | if (is_int($time) || is_float($time)) {
173 | try {
174 | return new DateTimeImmutable('@' . $time);
175 | } catch (Exception $e) {
176 | throw new LogicException('Invalid time value in log context: ' . $time . '.', previous: $e);
177 | }
178 | }
179 |
180 | if (is_string($time)) {
181 | $format = match (true) {
182 | str_contains($time, '.') => 'U.u',
183 | str_contains($time, ',') => 'U,u',
184 | default => 'U',
185 | };
186 | $date = DateTimeImmutable::createFromFormat($format, $time);
187 | if ($date === false) {
188 | throw new LogicException('Invalid time value in log context: "' . $time . '".');
189 | }
190 | return $date;
191 | }
192 |
193 | throw new LogicException('Invalid time value in log context. Got "' . get_debug_type($time) . '".');
194 | }
195 |
196 | /**
197 | * Parses log message resolving placeholders in the form: "{foo}",
198 | * where foo will be replaced by the context data in key "foo".
199 | *
200 | * @param string|Stringable $message Raw log message.
201 | * @param array $context Message context.
202 | *
203 | * @return string Parsed message.
204 | */
205 | private function parse(string|Stringable $message, array $context): string
206 | {
207 | $message = (string) $message;
208 |
209 | /** @var string */
210 | return preg_replace_callback(
211 | '/{(.*)}/',
212 | static function (array $matches) use ($context) {
213 | [$exist, $value] = ContextValueExtractor::extract($context, $matches[1]);
214 | if ($exist) {
215 | if (
216 | is_scalar($value)
217 | || $value instanceof Stringable
218 | || $value === null
219 | ) {
220 | return (string) $value;
221 | }
222 | return VarDumper::create($value)->asString();
223 | }
224 | return $matches[0];
225 | },
226 | $message
227 | );
228 | }
229 | }
230 |
--------------------------------------------------------------------------------
/src/Message/CategoryFilter.php:
--------------------------------------------------------------------------------
1 | checkStructure($categories);
58 | $this->include = $categories;
59 | }
60 |
61 | /**
62 | * Sets the log message categories to be excluded.
63 | *
64 | * @param string[] $categories The list of log message categories to be excluded.
65 | *
66 | * @throws InvalidArgumentException When log message category structure is invalid.
67 | */
68 | public function exclude(array $categories): void
69 | {
70 | $this->checkStructure($categories);
71 | $this->exclude = $categories;
72 | }
73 |
74 | /**
75 | * Checks whether the specified log message category is excluded.
76 | *
77 | * @param string $category The log message category.
78 | *
79 | * @return bool The value indicating whether the specified category is excluded.
80 | */
81 | public function isExcluded(string $category): bool
82 | {
83 | foreach ($this->exclude as $exclude) {
84 | $prefix = rtrim($exclude, '*');
85 |
86 | if ($category === $exclude || ($prefix !== $exclude && str_starts_with($category, $prefix))) {
87 | return true;
88 | }
89 | }
90 |
91 | if (empty($this->include)) {
92 | return false;
93 | }
94 |
95 | foreach ($this->include as $include) {
96 | if (
97 | $category === $include
98 | || (
99 | !empty($include)
100 | && str_ends_with($include, '*')
101 | && str_starts_with($category, rtrim($include, '*'))
102 | )
103 | ) {
104 | return false;
105 | }
106 | }
107 |
108 | return true;
109 | }
110 |
111 | /**
112 | * Checks message categories structure.
113 | *
114 | * @param array $categories The log message categories to be checked.
115 | *
116 | * @throws InvalidArgumentException When log message category structure is invalid.
117 | */
118 | private function checkStructure(array $categories): void
119 | {
120 | foreach ($categories as $category) {
121 | if (!is_string($category)) {
122 | throw new InvalidArgumentException(sprintf(
123 | 'The log message category must be a string, %s received.',
124 | get_debug_type($category)
125 | ));
126 | }
127 | }
128 | }
129 | }
130 |
--------------------------------------------------------------------------------
/src/Message/ContextValueExtractor.php:
--------------------------------------------------------------------------------
1 |
42 | */
43 | private static function parsePath(string $path): array
44 | {
45 | if ($path === '') {
46 | return [''];
47 | }
48 |
49 | if (!str_contains($path, '.')) {
50 | return [str_replace('\\\\', '\\', $path)];
51 | }
52 |
53 | /** @psalm-var non-empty-list $matches */
54 | $matches = preg_split(
55 | sprintf(
56 | '/(?%1$s%1$s)*)%2$s/',
57 | preg_quote('\\', '/'),
58 | preg_quote('.', '/')
59 | ),
60 | $path,
61 | -1,
62 | PREG_SPLIT_OFFSET_CAPTURE
63 | );
64 | $result = [];
65 | $countResults = count($matches);
66 | for ($i = 1; $i < $countResults; $i++) {
67 | $l = $matches[$i][1] - $matches[$i - 1][1] - strlen($matches[$i - 1][0]) - 1;
68 | $result[] = $matches[$i - 1][0] . ($l > 0 ? str_repeat('\\', $l) : '');
69 | }
70 | $result[] = $matches[$countResults - 1][0];
71 |
72 | return array_map(
73 | static fn(string $key): string => str_replace(
74 | [
75 | '\\\\',
76 | '\\.',
77 | ],
78 | [
79 | '\\',
80 | '.',
81 | ],
82 | $key
83 | ),
84 | $result
85 | );
86 | }
87 | }
88 |
--------------------------------------------------------------------------------
/src/Message/Formatter.php:
--------------------------------------------------------------------------------
1 | format = $format;
58 | }
59 |
60 | /**
61 | * Sets a PHP callable that returns a string to be prefixed to every exported message.
62 | *
63 | * @param callable $prefix The PHP callable to get a string prefix of the log message.
64 | *
65 | * @see Formatter::$prefix
66 | */
67 | public function setPrefix(callable $prefix): void
68 | {
69 | $this->prefix = $prefix;
70 | }
71 |
72 | /**
73 | * Sets a date format for the log timestamp.
74 | *
75 | * @param string $timestampFormat The date format for the log timestamp.
76 | *
77 | * @see Formatter::$timestampFormat
78 | */
79 | public function setTimestampFormat(string $timestampFormat): void
80 | {
81 | $this->timestampFormat = $timestampFormat;
82 | }
83 |
84 | /**
85 | * Formats a log message for display as a string.
86 | *
87 | * @param Message $message The log message to be formatted.
88 | * @param array $commonContext The user parameters in the `key => value` format.
89 | *
90 | * @throws RuntimeException for a callable "format" that does not return a string.
91 | *
92 | * @return string The formatted log message.
93 | */
94 | public function format(Message $message, array $commonContext): string
95 | {
96 | if ($this->format === null) {
97 | return $this->defaultFormat($message, $commonContext);
98 | }
99 |
100 | $formatted = ($this->format)($message, $commonContext);
101 |
102 | if (!is_string($formatted)) {
103 | throw new RuntimeException(sprintf(
104 | 'The PHP callable "format" must return a string, %s received.',
105 | get_debug_type($formatted)
106 | ));
107 | }
108 |
109 | return $this->getPrefix($message, $commonContext) . $formatted;
110 | }
111 |
112 | /**
113 | * Default formats a log message for display as a string.
114 | *
115 | * @param Message $message The log message to be default formatted.
116 | * @param array $commonContext The user parameters in the `key => value` format.
117 | *
118 | * @return string The default formatted log message.
119 | */
120 | private function defaultFormat(Message $message, array $commonContext): string
121 | {
122 | $time = $message->time()->format($this->timestampFormat);
123 | $prefix = $this->getPrefix($message, $commonContext);
124 | $context = $this->getContext($message, $commonContext);
125 |
126 | return "{$time} {$prefix}[{$message->level()}][{$message->category()}] {$message->message()}{$context}";
127 | }
128 |
129 | /**
130 | * Gets a string to be prefixed to the given message.
131 | *
132 | * If {@see Formatter::$prefix} is configured it will return the result of the callback.
133 | * The default implementation will return user IP, user ID and session ID as a prefix.
134 | *
135 | * @param Message $message The log message being exported.
136 | * @param array $commonContext The user parameters in the `key => value` format.
137 | *
138 | * @throws RuntimeException for a callable "prefix" that does not return a string.
139 | *
140 | * @return string The log prefix string.
141 | */
142 | private function getPrefix(Message $message, array $commonContext): string
143 | {
144 | if ($this->prefix === null) {
145 | return '';
146 | }
147 |
148 | $prefix = ($this->prefix)($message, $commonContext);
149 |
150 | if (!is_string($prefix)) {
151 | throw new RuntimeException(sprintf(
152 | 'The PHP callable "prefix" must return a string, %s received.',
153 | get_debug_type($prefix)
154 | ));
155 | }
156 |
157 | return $prefix;
158 | }
159 |
160 | /**
161 | * Gets the context information to be logged.
162 | *
163 | * @param Message $message The log message.
164 | * @param array $commonContext The user parameters in the `key => value` format.
165 | *
166 | * @return string The context information. If an empty string, it means no context information.
167 | */
168 | private function getContext(Message $message, array $commonContext): string
169 | {
170 | $trace = $this->getTrace($message);
171 | $context = [];
172 | $common = [];
173 |
174 | if ($trace !== '') {
175 | $context[] = $trace;
176 | }
177 |
178 | /**
179 | * @var array-key $name
180 | * @var mixed $value
181 | */
182 | foreach ($message->context() as $name => $value) {
183 | if ($name !== 'trace') {
184 | $context[] = "{$name}: " . $this->convertToString($value);
185 | }
186 | }
187 |
188 | /**
189 | * @var mixed $value
190 | */
191 | foreach ($commonContext as $name => $value) {
192 | $common[] = "{$name}: " . $this->convertToString($value);
193 | }
194 |
195 | return (empty($context) ? '' : "\n\nMessage context:\n\n" . implode("\n", $context))
196 | . (empty($common) ? '' : "\n\nCommon context:\n\n" . implode("\n", $common)) . "\n";
197 | }
198 |
199 | /**
200 | * Gets debug backtrace in string representation.
201 | *
202 | * @param Message $message The log message.
203 | *
204 | * @return string Debug backtrace in string representation.
205 | */
206 | private function getTrace(Message $message): string
207 | {
208 | $traces = $message->trace();
209 | if ($traces === null) {
210 | return '';
211 | }
212 |
213 | $lines = array_map(
214 | static function (mixed $trace): string {
215 | $file = $trace['file'] ?? null;
216 | $line = $trace['line'] ?? null;
217 | if (is_string($file) && is_int($line)) {
218 | return 'in ' . $file . ':' . $line;
219 | }
220 |
221 | $class = $trace['class'] ?? null;
222 | $function = $trace['function'] ?? null;
223 | if (is_string($function)) {
224 | return is_string($class)
225 | ? ($class . ':' . $function)
226 | : $function;
227 | }
228 |
229 | return '???';
230 | },
231 | $traces,
232 | );
233 |
234 | return "trace:\n " . implode("\n ", $lines);
235 | }
236 |
237 | /**
238 | * Converts a value to a string.
239 | *
240 | * @param mixed $value The value to convert.
241 | *
242 | * @return string Converted string.
243 | */
244 | private function convertToString(mixed $value): string
245 | {
246 | if (is_object($value) && method_exists($value, '__toString')) {
247 | return (string) $value;
248 | }
249 |
250 | return VarDumper::create($value)->asString();
251 | }
252 | }
253 |
--------------------------------------------------------------------------------
/src/PsrTarget.php:
--------------------------------------------------------------------------------
1 | logger;
30 | }
31 |
32 | protected function export(): void
33 | {
34 | foreach ($this->getMessages() as $message) {
35 | /** @var array $context */
36 | $context = $message->context();
37 | $this->logger->log($message->level(), $message->message(), $context);
38 | }
39 | }
40 | }
41 |
--------------------------------------------------------------------------------
/src/StreamTarget.php:
--------------------------------------------------------------------------------
1 | createStream();
39 | flock($stream, LOCK_EX);
40 |
41 | if (fwrite($stream, $this->formatMessages("\n")) === false) {
42 | flock($stream, LOCK_UN);
43 | fclose($stream);
44 | throw new RuntimeException(sprintf(
45 | 'Unable to export the log because of an error writing to the stream: %s',
46 | error_get_last()['message'] ?? '',
47 | ));
48 | }
49 |
50 | $this->stream = stream_get_meta_data($stream)['uri'];
51 | flock($stream, LOCK_UN);
52 | fclose($stream);
53 | }
54 |
55 | /**
56 | * Check and create a stream resource.
57 | *
58 | * @throws RuntimeException if the stream cannot be opened.
59 | * @throws InvalidArgumentException if the stream is invalid.
60 | *
61 | * @return resource The stream resource.
62 | */
63 | private function createStream()
64 | {
65 | $stream = $this->stream;
66 |
67 | if (is_string($stream)) {
68 | $stream = @fopen($stream, 'ab');
69 | if ($stream === false) {
70 | throw new RuntimeException(sprintf(
71 | 'The "%s" stream cannot be opened.',
72 | (string) $this->stream,
73 | ));
74 | }
75 | }
76 |
77 | /** @psalm-suppress DocblockTypeContradiction */
78 | if (!is_resource($stream) || get_resource_type($stream) !== 'stream') {
79 | throw new InvalidArgumentException(sprintf(
80 | 'Invalid stream provided. It must be a string stream identifier or a stream resource, "%s" received.',
81 | get_debug_type($stream),
82 | ));
83 | }
84 |
85 | return $stream;
86 | }
87 | }
88 |
--------------------------------------------------------------------------------
/src/Target.php:
--------------------------------------------------------------------------------
1 | value` format that should be logged in a each message.
60 | */
61 | private array $commonContext = [];
62 |
63 | /**
64 | * @var int How many log messages should be accumulated before they are exported.
65 | *
66 | * Defaults to 1000. Note that messages will always be exported when the application terminates.
67 | * Set this property to be 0 if you don't want to export messages until the application terminates.
68 | */
69 | private int $exportInterval = 1000;
70 |
71 | /**
72 | * @var bool|callable Enables or disables the current target to export.
73 | */
74 | private $enabled = true;
75 |
76 | /**
77 | * Exports log messages to a specific destination.
78 | * Child classes must implement this method.
79 | */
80 | abstract protected function export(): void;
81 |
82 | /**
83 | * When defining a constructor in child classes, you must call `parent::__construct()`.
84 | */
85 | public function __construct()
86 | {
87 | $this->categories = new CategoryFilter();
88 | $this->formatter = new Formatter();
89 | }
90 |
91 | /**
92 | * Processes the given log messages.
93 | *
94 | * This method will filter the given messages with levels and categories.
95 | * And if requested, it will also export the filtering result to specific medium (e.g. email).
96 | *
97 | * @param Message[] $messages Log messages to be processed.
98 | * @param bool $final Whether this method is called at the end of the current application.
99 | */
100 | public function collect(array $messages, bool $final): void
101 | {
102 | $this->filterMessages($messages);
103 | $count = count($this->messages);
104 |
105 | if ($count > 0 && ($final || ($this->exportInterval > 0 && $count >= $this->exportInterval))) {
106 | // set exportInterval to 0 to avoid triggering export again while exporting
107 | $oldExportInterval = $this->exportInterval;
108 | $this->exportInterval = 0;
109 | $this->export();
110 | $this->exportInterval = $oldExportInterval;
111 | $this->messages = [];
112 | }
113 | }
114 |
115 | /**
116 | * Sets a list of log message categories that this target is interested in.
117 | *
118 | * @param string[] $categories The list of log message categories.
119 | *
120 | * @throws InvalidArgumentException for invalid log message categories structure.
121 | *
122 | * @return self
123 | *
124 | * @see CategoryFilter::$include
125 | */
126 | public function setCategories(array $categories): self
127 | {
128 | $this->categories->include($categories);
129 | return $this;
130 | }
131 |
132 | /**
133 | * Sets a list of log message categories that this target is NOT interested in.
134 | *
135 | * @param string[] $except The list of log message categories.
136 | *
137 | * @throws InvalidArgumentException for invalid log message categories structure.
138 | *
139 | * @return self
140 | *
141 | * @see CategoryFilter::$exclude
142 | */
143 | public function setExcept(array $except): self
144 | {
145 | $this->categories->exclude($except);
146 | return $this;
147 | }
148 |
149 | /**
150 | * Sets a list of log message levels that current target is interested in.
151 | *
152 | * @param string[] $levels The list of log message levels.
153 | *
154 | * @throws InvalidArgumentException for invalid log message level.
155 | *
156 | * @return self
157 | *
158 | * @see Target::$levels
159 | */
160 | public function setLevels(array $levels): self
161 | {
162 | foreach ($levels as $key => $level) {
163 | Logger::assertLevelIsValid($level);
164 | $levels[$key] = $level;
165 | }
166 |
167 | $this->levels = $levels;
168 | return $this;
169 | }
170 |
171 | /**
172 | * Sets a user parameters in the `key => value` format that should be logged in a each message.
173 | *
174 | * @param array $commonContext The user parameters in the `key => value` format.
175 | *
176 | * @return self
177 | *
178 | * @see Target::$commonContext
179 | *
180 | * @deprecated since 2.1, to be removed in 3.0. Use {@see CommonContextProvider} instead.
181 | */
182 | public function setCommonContext(array $commonContext): self
183 | {
184 | $this->commonContext = $commonContext;
185 | return $this;
186 | }
187 |
188 | /**
189 | * Sets a PHP callable that returns a string representation of the log message.
190 | *
191 | * @param callable $format The PHP callable to get a string value from.
192 | *
193 | * @return self
194 | *
195 | * @see Formatter::$format
196 | */
197 | public function setFormat(callable $format): self
198 | {
199 | $this->formatter->setFormat($format);
200 | return $this;
201 | }
202 |
203 | /**
204 | * Sets a PHP callable that returns a string to be prefixed to every exported message.
205 | *
206 | * @param callable $prefix The PHP callable to get a string prefix of the log message.
207 | *
208 | * @return self
209 | *
210 | * @see Formatter::$prefix
211 | */
212 | public function setPrefix(callable $prefix): self
213 | {
214 | $this->formatter->setPrefix($prefix);
215 | return $this;
216 | }
217 |
218 | /**
219 | * Sets how many messages should be accumulated before they are exported.
220 | *
221 | * @param int $exportInterval The number of log messages to accumulate before exporting.
222 | *
223 | * @return self
224 | *
225 | * @see Target::$exportInterval
226 | */
227 | public function setExportInterval(int $exportInterval): self
228 | {
229 | $this->exportInterval = $exportInterval;
230 | return $this;
231 | }
232 |
233 | /**
234 | * Sets a date format for the log timestamp.
235 | *
236 | * @param string $format The date format for the log timestamp.
237 | *
238 | * @return self
239 | *
240 | * @see Target::$timestampFormat
241 | */
242 | public function setTimestampFormat(string $format): self
243 | {
244 | $this->formatter->setTimestampFormat($format);
245 | return $this;
246 | }
247 |
248 | /**
249 | * Sets a PHP callable that returns a boolean indicating whether this log target is enabled.
250 | *
251 | * The signature of the callable should be `function (): bool;`.
252 | *
253 | * @param callable $value The PHP callable to get a boolean value.
254 | *
255 | * @return self
256 | *
257 | * @see Target::$enabled
258 | */
259 | public function setEnabled(callable $value): self
260 | {
261 | $this->enabled = $value;
262 | return $this;
263 | }
264 |
265 | /**
266 | * Enables the log target.
267 | *
268 | * @return self
269 | *
270 | * @see Target::$enabled
271 | */
272 | public function enable(): self
273 | {
274 | $this->enabled = true;
275 | return $this;
276 | }
277 |
278 | /**
279 | * Disables the log target.
280 | *
281 | * @return self
282 | *
283 | * @see Target::$enabled
284 | */
285 | public function disable(): self
286 | {
287 | $this->enabled = false;
288 | return $this;
289 | }
290 |
291 | /**
292 | * Check whether the log target is enabled.
293 | *
294 | * @throws RuntimeException for a callable "enabled" that does not return a boolean.
295 | *
296 | * @return bool The value indicating whether this log target is enabled.
297 | *
298 | * @see Target::$enabled
299 | */
300 | public function isEnabled(): bool
301 | {
302 | if (is_bool($this->enabled)) {
303 | return $this->enabled;
304 | }
305 |
306 | if (!is_bool($enabled = ($this->enabled)())) {
307 | throw new RuntimeException(sprintf(
308 | 'The PHP callable "enabled" must returns a boolean, %s received.',
309 | get_debug_type($enabled)
310 | ));
311 | }
312 |
313 | return $enabled;
314 | }
315 |
316 | /**
317 | * Gets a list of log messages that are retrieved from the logger so far by this log target.
318 | *
319 | * @return Message[] The list of log messages.
320 | */
321 | protected function getMessages(): array
322 | {
323 | return $this->messages;
324 | }
325 |
326 | /**
327 | * Gets a list of formatted log messages.
328 | *
329 | * @return string[] The list of formatted log messages.
330 | */
331 | protected function getFormattedMessages(): array
332 | {
333 | $formatted = [];
334 |
335 | foreach ($this->messages as $key => $message) {
336 | $formatted[$key] = $this->formatter->format($message, $this->commonContext);
337 | }
338 |
339 | return $formatted;
340 | }
341 |
342 | /**
343 | * Formats all log messages for display as a string.
344 | *
345 | * @param string $separator The log messages string separator.
346 | *
347 | * @return string The string formatted log messages.
348 | */
349 | protected function formatMessages(string $separator = ''): string
350 | {
351 | $formatted = '';
352 |
353 | foreach ($this->messages as $message) {
354 | $formatted .= $this->formatter->format($message, $this->commonContext) . $separator;
355 | }
356 |
357 | return $formatted;
358 | }
359 |
360 | /**
361 | * Gets a user parameters in the `key => value` format that should be logged in a each message.
362 | *
363 | * @return array The user parameters in the `key => value` format.
364 | *
365 | * @deprecated since 2.1, to be removed in 3.0. Use {@see CommonContextProvider} instead.
366 | */
367 | protected function getCommonContext(): array
368 | {
369 | return $this->commonContext;
370 | }
371 |
372 | /**
373 | * Filters the given messages according to their categories and levels.
374 | *
375 | * @param array $messages List log messages to be filtered.
376 | *
377 | * @throws InvalidArgumentException for non-instance Message.
378 | */
379 | private function filterMessages(array $messages): void
380 | {
381 | foreach ($messages as $i => $message) {
382 | if (!($message instanceof Message)) {
383 | throw new InvalidArgumentException('You must provide an instance of \Yiisoft\Log\Message.');
384 | }
385 |
386 | if (!empty($this->levels) && !in_array($message->level(), $this->levels, true)) {
387 | unset($messages[$i]);
388 | continue;
389 | }
390 |
391 | if ($this->categories->isExcluded($message->category())) {
392 | unset($messages[$i]);
393 | continue;
394 | }
395 |
396 | $this->messages[] = $message;
397 | }
398 | }
399 | }
400 |
--------------------------------------------------------------------------------