├── .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 | Yii 4 | 5 |

Yii Logging Library

6 |
7 |

8 | 9 | [![Latest Stable Version](https://poser.pugx.org/yiisoft/log/v)](https://packagist.org/packages/yiisoft/log) 10 | [![Total Downloads](https://poser.pugx.org/yiisoft/log/downloads)](https://packagist.org/packages/yiisoft/log) 11 | [![Build status](https://github.com/yiisoft/log/actions/workflows/build.yml/badge.svg)](https://github.com/yiisoft/log/actions/workflows/build.yml) 12 | [![Code coverage](https://codecov.io/gh/yiisoft/log/graph/badge.svg?token=4CSPCRMGQM)](https://codecov.io/gh/yiisoft/log) 13 | [![Mutation testing badge](https://img.shields.io/endpoint?style=flat&url=https%3A%2F%2Fbadge-api.stryker-mutator.io%2Fgithub.com%2Fyiisoft%2Flog%2Fmaster)](https://dashboard.stryker-mutator.io/reports/github.com/yiisoft/log/master) 14 | [![static analysis](https://github.com/yiisoft/log/workflows/static%20analysis/badge.svg)](https://github.com/yiisoft/log/actions?query=workflow%3A%22static+analysis%22) 15 | [![type-coverage](https://shepherd.dev/github/yiisoft/log/coverage.svg)](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 | [![Open Collective](https://img.shields.io/badge/Open%20Collective-sponsor-7eadf1?logo=open%20collective&logoColor=7eadf1&labelColor=555555)](https://opencollective.com/yiisoft) 182 | 183 | ## Follow updates 184 | 185 | [![Official website](https://img.shields.io/badge/Powered_by-Yii_Framework-green.svg?style=flat)](https://www.yiiframework.com/) 186 | [![Twitter](https://img.shields.io/badge/twitter-follow-1DA1F2?logo=twitter&logoColor=1DA1F2&labelColor=555555?style=flat)](https://twitter.com/yiiframework) 187 | [![Telegram](https://img.shields.io/badge/telegram-join-1DA1F2?style=flat&logo=telegram)](https://t.me/yii3en) 188 | [![Facebook](https://img.shields.io/badge/facebook-join-1DA1F2?style=flat&logo=facebook&logoColor=ffffff)](https://www.facebook.com/groups/yiitalk) 189 | [![Slack](https://img.shields.io/badge/slack-join-1DA1F2?style=flat&logo=slack)](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 | --------------------------------------------------------------------------------