1 | <?php |
||||
2 | /** |
||||
3 | * @link http://www.yiiframework.com/ |
||||
4 | * @copyright Copyright (c) 2008 Yii Software LLC |
||||
5 | * @license http://www.yiiframework.com/license/ |
||||
6 | */ |
||||
7 | |||||
8 | namespace yii\log; |
||||
9 | |||||
10 | use Yii; |
||||
11 | use yii\base\InvalidConfigException; |
||||
12 | use yii\helpers\FileHelper; |
||||
13 | |||||
14 | /** |
||||
15 | * FileTarget records log messages in a file. |
||||
16 | * |
||||
17 | * The log file is specified via [[logFile]]. If the size of the log file exceeds |
||||
18 | * [[maxFileSize]] (in kilo-bytes), a rotation will be performed, which renames |
||||
19 | * the current log file by suffixing the file name with '.1'. All existing log |
||||
20 | * files are moved backwards by one place, i.e., '.2' to '.3', '.1' to '.2', and so on. |
||||
21 | * The property [[maxLogFiles]] specifies how many history files to keep. |
||||
22 | * |
||||
23 | * @author Qiang Xue <[email protected]> |
||||
24 | * @since 2.0 |
||||
25 | */ |
||||
26 | class FileTarget extends Target |
||||
27 | { |
||||
28 | /** |
||||
29 | * @var string log file path or [path alias](guide:concept-aliases). If not set, it will use the "@runtime/logs/app.log" file. |
||||
30 | * The directory containing the log files will be automatically created if not existing. |
||||
31 | */ |
||||
32 | public $logFile; |
||||
33 | /** |
||||
34 | * @var bool whether log files should be rotated when they reach a certain [[maxFileSize|maximum size]]. |
||||
35 | * Log rotation is enabled by default. This property allows you to disable it, when you have configured |
||||
36 | * an external tools for log rotation on your server. |
||||
37 | * @since 2.0.3 |
||||
38 | */ |
||||
39 | public $enableRotation = true; |
||||
40 | /** |
||||
41 | * @var int maximum log file size, in kilo-bytes. Defaults to 10240, meaning 10MB. |
||||
42 | */ |
||||
43 | public $maxFileSize = 10240; // in KB |
||||
44 | /** |
||||
45 | * @var int number of log files used for rotation. Defaults to 5. |
||||
46 | */ |
||||
47 | public $maxLogFiles = 5; |
||||
48 | /** |
||||
49 | * @var int the permission to be set for newly created log files. |
||||
50 | * This value will be used by PHP chmod() function. No umask will be applied. |
||||
51 | * If not set, the permission will be determined by the current environment. |
||||
52 | */ |
||||
53 | public $fileMode; |
||||
54 | /** |
||||
55 | * @var int the permission to be set for newly created directories. |
||||
56 | * This value will be used by PHP chmod() function. No umask will be applied. |
||||
57 | * Defaults to 0775, meaning the directory is read-writable by owner and group, |
||||
58 | * but read-only for other users. |
||||
59 | */ |
||||
60 | public $dirMode = 0775; |
||||
61 | /** |
||||
62 | * @var bool Whether to rotate log files by copy and truncate in contrast to rotation by |
||||
63 | * renaming files. Defaults to `true` to be more compatible with log tailers and is windows |
||||
64 | * systems which do not play well with rename on open files. Rotation by renaming however is |
||||
65 | * a bit faster. |
||||
66 | * |
||||
67 | * The problem with windows systems where the [rename()](https://secure.php.net/manual/en/function.rename.php) |
||||
68 | * function does not work with files that are opened by some process is described in a |
||||
69 | * [comment by Martin Pelletier](https://secure.php.net/manual/en/function.rename.php#102274) in |
||||
70 | * the PHP documentation. By setting rotateByCopy to `true` you can work |
||||
71 | * around this problem. |
||||
72 | */ |
||||
73 | public $rotateByCopy = true; |
||||
74 | |||||
75 | |||||
76 | /** |
||||
77 | * Initializes the route. |
||||
78 | * This method is invoked after the route is created by the route manager. |
||||
79 | */ |
||||
80 | 3 | public function init() |
|||
81 | { |
||||
82 | 3 | parent::init(); |
|||
83 | 3 | if ($this->logFile === null) { |
|||
84 | $this->logFile = Yii::$app->getRuntimePath() . '/logs/app.log'; |
||||
85 | } else { |
||||
86 | 3 | $this->logFile = Yii::getAlias($this->logFile); |
|||
87 | } |
||||
88 | 3 | if ($this->maxLogFiles < 1) { |
|||
89 | $this->maxLogFiles = 1; |
||||
90 | } |
||||
91 | 3 | if ($this->maxFileSize < 1) { |
|||
92 | $this->maxFileSize = 1; |
||||
93 | } |
||||
94 | 3 | } |
|||
95 | |||||
96 | /** |
||||
97 | * Writes log messages to a file. |
||||
98 | * Starting from version 2.0.14, this method throws LogRuntimeException in case the log can not be exported. |
||||
99 | * @throws InvalidConfigException if unable to open the log file for writing |
||||
100 | * @throws LogRuntimeException if unable to write complete log to file |
||||
101 | */ |
||||
102 | 2 | public function export() |
|||
103 | { |
||||
104 | 2 | if (strpos($this->logFile, '://') === false || strncmp($this->logFile, 'file://', 7) === 0) { |
|||
105 | 2 | $logPath = dirname($this->logFile); |
|||
106 | 2 | FileHelper::createDirectory($logPath, $this->dirMode, true); |
|||
107 | } |
||||
108 | |||||
109 | 2 | $text = implode("\n", array_map([$this, 'formatMessage'], $this->messages)) . "\n"; |
|||
110 | 2 | if (($fp = @fopen($this->logFile, 'a')) === false) { |
|||
111 | throw new InvalidConfigException("Unable to append to log file: {$this->logFile}"); |
||||
112 | } |
||||
113 | 2 | @flock($fp, LOCK_EX); |
|||
114 | 2 | if ($this->enableRotation) { |
|||
115 | // clear stat cache to ensure getting the real current file size and not a cached one |
||||
116 | // this may result in rotating twice when cached file size is used on subsequent calls |
||||
117 | 2 | clearstatcache(); |
|||
118 | } |
||||
119 | 2 | if ($this->enableRotation && @filesize($this->logFile) > $this->maxFileSize * 1024) { |
|||
120 | 2 | @flock($fp, LOCK_UN); |
|||
121 | 2 | @fclose($fp); |
|||
122 | 2 | $this->rotateFiles(); |
|||
123 | 2 | $writeResult = @file_put_contents($this->logFile, $text, FILE_APPEND | LOCK_EX); |
|||
124 | 2 | if ($writeResult === false) { |
|||
125 | $error = error_get_last(); |
||||
126 | throw new LogRuntimeException("Unable to export log through file ({$this->logFile})!: {$error['message']}"); |
||||
127 | } |
||||
128 | 2 | $textSize = strlen($text); |
|||
129 | 2 | if ($writeResult < $textSize) { |
|||
130 | 2 | throw new LogRuntimeException("Unable to export whole log through file ({$this->logFile})! Wrote $writeResult out of $textSize bytes."); |
|||
131 | } |
||||
132 | } else { |
||||
133 | 2 | $writeResult = @fwrite($fp, $text); |
|||
134 | 2 | if ($writeResult === false) { |
|||
135 | $error = error_get_last(); |
||||
136 | throw new LogRuntimeException("Unable to export log through file ({$this->logFile})!: {$error['message']}"); |
||||
137 | } |
||||
138 | 2 | $textSize = strlen($text); |
|||
139 | 2 | if ($writeResult < $textSize) { |
|||
140 | throw new LogRuntimeException("Unable to export whole log through file ({$this->logFile})! Wrote $writeResult out of $textSize bytes."); |
||||
141 | } |
||||
142 | 2 | @flock($fp, LOCK_UN); |
|||
143 | 2 | @fclose($fp); |
|||
144 | } |
||||
145 | 2 | if ($this->fileMode !== null) { |
|||
146 | @chmod($this->logFile, $this->fileMode); |
||||
147 | } |
||||
148 | 2 | } |
|||
149 | |||||
150 | /** |
||||
151 | * Rotates log files. |
||||
152 | */ |
||||
153 | 2 | protected function rotateFiles() |
|||
154 | { |
||||
155 | 2 | $file = $this->logFile; |
|||
156 | 2 | for ($i = $this->maxLogFiles; $i >= 0; --$i) { |
|||
157 | // $i == 0 is the original log file |
||||
158 | 2 | $rotateFile = $file . ($i === 0 ? '' : '.' . $i); |
|||
159 | 2 | if (is_file($rotateFile)) { |
|||
160 | // suppress errors because it's possible multiple processes enter into this section |
||||
161 | 2 | if ($i === $this->maxLogFiles) { |
|||
162 | 2 | @unlink($rotateFile); |
|||
163 | 2 | continue; |
|||
164 | } |
||||
165 | 2 | $newFile = $this->logFile . '.' . ($i + 1); |
|||
166 | 2 | $this->rotateByCopy ? $this->rotateByCopy($rotateFile, $newFile) : $this->rotateByRename($rotateFile, $newFile); |
|||
167 | 2 | if ($i === 0) { |
|||
168 | 2 | $this->clearLogFile($rotateFile); |
|||
169 | } |
||||
170 | } |
||||
171 | } |
||||
172 | 2 | } |
|||
173 | |||||
174 | /*** |
||||
175 | * Clear log file without closing any other process open handles |
||||
176 | * @param string $rotateFile |
||||
177 | */ |
||||
178 | 2 | private function clearLogFile($rotateFile) |
|||
179 | { |
||||
180 | 2 | if ($filePointer = @fopen($rotateFile, 'a')) { |
|||
181 | 2 | @ftruncate($filePointer, 0); |
|||
0 ignored issues
–
show
|
|||||
182 | 2 | @fclose($filePointer); |
|||
0 ignored issues
–
show
It seems like you do not handle an error condition for
fclose() . This can introduce security issues, and is generally not recommended.
(
Ignorable by Annotation
)
If this is a false-positive, you can also ignore this issue in your code via the
If you suppress an error, we recommend checking for the error condition explicitly: // For example instead of
@mkdir($dir);
// Better use
if (@mkdir($dir) === false) {
throw new \RuntimeException('The directory '.$dir.' could not be created.');
}
![]() |
|||||
183 | } |
||||
184 | 2 | } |
|||
185 | |||||
186 | /*** |
||||
187 | * Copy rotated file into new file |
||||
188 | * @param string $rotateFile |
||||
189 | * @param string $newFile |
||||
190 | */ |
||||
191 | 1 | private function rotateByCopy($rotateFile, $newFile) |
|||
192 | { |
||||
193 | 1 | @copy($rotateFile, $newFile); |
|||
0 ignored issues
–
show
It seems like you do not handle an error condition for
copy() . This can introduce security issues, and is generally not recommended.
(
Ignorable by Annotation
)
If this is a false-positive, you can also ignore this issue in your code via the
If you suppress an error, we recommend checking for the error condition explicitly: // For example instead of
@mkdir($dir);
// Better use
if (@mkdir($dir) === false) {
throw new \RuntimeException('The directory '.$dir.' could not be created.');
}
![]() |
|||||
194 | 1 | if ($this->fileMode !== null) { |
|||
195 | @chmod($newFile, $this->fileMode); |
||||
0 ignored issues
–
show
It seems like you do not handle an error condition for
chmod() . This can introduce security issues, and is generally not recommended.
(
Ignorable by Annotation
)
If this is a false-positive, you can also ignore this issue in your code via the
If you suppress an error, we recommend checking for the error condition explicitly: // For example instead of
@mkdir($dir);
// Better use
if (@mkdir($dir) === false) {
throw new \RuntimeException('The directory '.$dir.' could not be created.');
}
![]() |
|||||
196 | } |
||||
197 | 1 | } |
|||
198 | |||||
199 | /** |
||||
200 | * Renames rotated file into new file |
||||
201 | * @param string $rotateFile |
||||
202 | * @param string $newFile |
||||
203 | */ |
||||
204 | 1 | private function rotateByRename($rotateFile, $newFile) |
|||
205 | { |
||||
206 | 1 | @rename($rotateFile, $newFile); |
|||
0 ignored issues
–
show
It seems like you do not handle an error condition for
rename() . This can introduce security issues, and is generally not recommended.
(
Ignorable by Annotation
)
If this is a false-positive, you can also ignore this issue in your code via the
If you suppress an error, we recommend checking for the error condition explicitly: // For example instead of
@mkdir($dir);
// Better use
if (@mkdir($dir) === false) {
throw new \RuntimeException('The directory '.$dir.' could not be created.');
}
![]() |
|||||
207 | 1 | } |
|||
208 | } |
||||
209 |
If you suppress an error, we recommend checking for the error condition explicitly: