Passed
Push — dev ( a02dd0...b83e75 )
by 世昌
02:23
created

FileLogger::packLogFile()   A

Complexity

Conditions 2
Paths 2

Size

Total Lines 13
Code Lines 10

Duplication

Lines 0
Ratio 0 %

Importance

Changes 4
Bugs 0 Features 0
Metric Value
cc 2
eloc 10
nc 2
nop 0
dl 0
loc 13
rs 9.9332
c 4
b 0
f 0
1
<?php
2
3
namespace suda\framework\debug\log\logger;
4
5
use ZipArchive;
6
use RecursiveIteratorIterator;
7
use RecursiveDirectoryIterator;
8
use suda\framework\debug\ConfigTrait;
9
use suda\framework\debug\log\LogLevel;
10
use suda\framework\debug\ConfigInterface;
11
use suda\framework\filesystem\FileSystem;
12
use suda\framework\debug\log\AbstractLogger;
13
use suda\framework\debug\log\logger\exception\FileLoggerException;
14
15
/**
16
 * Class FileLogger
17
 * @package suda\framework\debug\log\logger
18
 */
19
class FileLogger extends AbstractLogger implements ConfigInterface
20
{
21
    use ConfigTrait;
22
23
    /**
24
     * 文件
25
     *
26
     * @var resource
27
     */
28
    protected $temp;
29
30
    /**
31
     * 临时文件名
32
     *
33
     * @var string
34
     */
35
    protected $tempName;
36
37
    /**
38
     * 移除文件
39
     *
40
     * @var array
41
     */
42
    protected $removeFiles = [];
43
44
    /**
45
     * 最后的日志
46
     *
47
     * @var string
48
     */
49
    protected $latest;
50
51
    /**
52
     * 构建文件日志
53
     *
54
     * @param array $config
55
     */
56
    public function __construct(array $config = [])
57
    {
58
        $this->set($config);
59
        register_shutdown_function([$this, 'shutdown']);
60
    }
61
62
    /**
63
     * 设置配置
64
     * @param array $config
65
     */
66
    public function set(array $config)
67
    {
68
        $this->applyConfig($config);
69
        FileSystem::make($this->getConfig('save-path'));
70
        FileSystem::make($this->getConfig('save-dump-path'));
71
        FileSystem::make($this->getConfig('save-zip-path'));
72
    }
73
74
    /**
75
     * @return resource
76
     * @throws FileLoggerException
77
     */
78
    public function getAvailableWrite()
79
    {
80
        if (is_resource($this->temp)) {
81
            return $this->temp;
82
        }
83
        $this->prepareWrite();
84
        return $this->temp;
85
    }
86
87
    /**
88
     * @throws FileLoggerException
89
     */
90
    private function prepareWrite()
91
    {
92
        $unique = substr(md5(uniqid()), 0, 8);
93
        $save = $this->getConfig('save-path');
94
        $this->tempName = $save . '/' . date('YmdHis') . '.' . $unique . '.log';
95
        $temp = fopen($this->tempName, 'w+');
96
        if ($temp !== false) {
97
            $this->temp = $temp;
98
        } else {
99
            throw new FileLoggerException(__METHOD__ . ':' . sprintf('cannot create log file'));
100
        }
101
        $this->latest = $save . '/' . $this->getConfig('file-name');
102
    }
103
104
    /**
105
     * @return array
106
     */
107
    public function getDefaultConfig(): array
108
    {
109
        return [
110
            'save-path' => './logs',
111
            'save-zip-path' => './logs/zip',
112
            'save-dump-path' => './logs/dump',
113
            'max-file-size' => 2097152,
114
            'file-name' => 'latest.log',
115
            'log-level' => 'debug',
116
            'log-format' => '[%level%] %message%',
117
        ];
118
    }
119
120
    /**
121
     * 打包文件
122
     */
123
    private function packLogFile()
124
    {
125
        $logFile = $this->latest;
126
        $path = preg_replace(
127
            '/[\\\\]+/',
128
            '/',
129
            $this->getConfig('save-zip-path') . '/' . date('Y-m-d') . '.zip'
130
        );
131
        $zip = $this->getZipArchive($path);
132
        if ($zip !== null) {
133
            $this->zipFile($zip, $logFile);
134
        } else {
135
            $this->moveFile($logFile);
136
        }
137
    }
138
139
    /**
140
     * @param ZipArchive $zip
141
     * @param string $logFile
142
     */
143
    private function zipFile(ZipArchive $zip, string $logFile)
144
    {
145
        $add = $zip->addFile($logFile, date('Y-m-d') . '-' . $zip->numFiles . '.log');
146
        if (is_dir($this->getConfig('save-dump-path'))) {
147
            $it = new RecursiveIteratorIterator(new RecursiveDirectoryIterator(
148
                $this->getConfig('save-dump-path'),
149
                RecursiveDirectoryIterator::SKIP_DOTS
150
            ));
151
            foreach ($it as $dumpLog) {
152
                if ($zip->addFile($dumpLog, 'dump/' . basename($dumpLog))) {
153
                    array_push($this->removeFiles, $dumpLog);
154
                }
155
            }
156
        }
157
        $zip->close();
158
        if ($add) {
159
            $this->clearContent($logFile);
160
        }
161
    }
162
163
    /**
164
     * @param string $logFile
165
     */
166
    private function moveFile(string $logFile)
167
    {
168
        if (is_file($logFile) && file_exists($logFile)) {
169
            $this->safeMoveKeep(
170
                $logFile,
171
                $this->getConfig('save-path')
172
                . '/' . date('Y-m-d')
173
                . '-' . substr(md5(uniqid()), 0, 8) . '.log'
174
            );
175
        }
176
    }
177
178
    /**
179
     * @param string $from
180
     * @param string $to
181
     */
182
    private function safeMoveKeep(string $from, string $to)
183
    {
184
        $fromFile = fopen($from, 'r');
185
        $toFile = fopen($to, 'w+');
186
        if ($fromFile !== false && $toFile !== false) {
187
            flock($toFile, LOCK_EX);
188
            // 复制内容
189
            stream_copy_to_stream($fromFile, $toFile);
190
            flock($toFile, LOCK_UN);
191
            fclose($toFile);
192
            fclose($fromFile);
193
        }
194
        // 清空内容
195
        $this->clearContent($from);
196
    }
197
198
    /**
199
     * 清空内容
200
     * @param string $path
201
     * @return bool
202
     */
203
    private function clearContent(string $path)
204
    {
205
        $file = fopen($path, 'w');
206
        if ($file !== false) {
207
            fclose($file);
208
            return true;
209
        }
210
        return false;
211
    }
212
213
    /**
214
     * 获取压缩
215
     *
216
     * @param string $path
217
     * @return ZipArchive|null
218
     */
219
    private function getZipArchive(string $path)
220
    {
221
        if (class_exists('ZipArchive')) {
222
            $zip = new ZipArchive;
223
            $res = $zip->open($path, ZipArchive::CREATE);
224
            if ($res === true) {
225
                return $zip;
226
            }
227
        }
228
        return null;
229
    }
230
231
    /**
232
     * 检查日志文件大小
233
     *
234
     * @return boolean
235
     */
236
    private function checkSize(): bool
237
    {
238
        $logFile = $this->latest;
239
        if (file_exists($logFile)) {
240
            if (filesize($logFile) > $this->getConfig('max-file-size')) {
241
                return true;
242
            }
243
        }
244
        return false;
245
    }
246
247
248
    /**
249
     * @param string $level
250
     * @param string $message
251
     * @param array $context
252
     * @return mixed|void
253
     * @throws FileLoggerException
254
     */
255
    public function log($level, string $message, array $context = [])
256
    {
257
        if (LogLevel::compare($level, $this->getConfig('log-level')) >= 0) {
258
            $replace = [];
259
            $message = $this->interpolate($message, $context);
260
            $replace['%level%'] = $level;
261
            $replace['%message%'] = $message;
262
            $write = strtr($this->getConfig('log-format'), $replace);
263
            fwrite($this->getAvailableWrite(), $write . PHP_EOL);
264
        }
265
    }
266
267
268
    /**
269
     * 将临时文件写入最后日志
270
     */
271
    private function rollLatest()
272
    {
273
        if (isset($this->latest)) {
274
            $latest = fopen($this->latest, 'a+');
275
            if (flock($latest, LOCK_EX)) {
0 ignored issues
show
Bug introduced by
It seems like $latest can also be of type false; however, parameter $handle of flock() does only seem to accept resource, maybe add an additional type check? ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

275
            if (flock(/** @scrutinizer ignore-type */ $latest, LOCK_EX)) {
Loading history...
276
                rewind($this->temp);
277
                stream_copy_to_stream($this->temp, $latest);
0 ignored issues
show
Bug introduced by
It seems like $latest can also be of type false; however, parameter $dest of stream_copy_to_stream() does only seem to accept resource, maybe add an additional type check? ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

277
                stream_copy_to_stream($this->temp, /** @scrutinizer ignore-type */ $latest);
Loading history...
278
                flock($latest, LOCK_UN);
279
                if (file_exists($this->tempName)) {
280
                    unlink($this->tempName);
281
                }
282
            }
283
            fclose($latest);
0 ignored issues
show
Bug introduced by
It seems like $latest can also be of type false; however, parameter $handle of fclose() does only seem to accept resource, maybe add an additional type check? ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

283
            fclose(/** @scrutinizer ignore-type */ $latest);
Loading history...
284
            fclose($this->temp);
285
            $this->temp = null;
286
            $this->tempName = null;
287
        }
288
    }
289
290
    /**
291
     * 删除已经压缩的文件
292
     */
293
    private function removePackFiles()
294
    {
295
        foreach ($this->removeFiles as $file) {
296
            if (is_file($file) && file_exists($file)) {
297
                unlink($file);
298
            }
299
        }
300
        $this->removeFiles = [];
301
    }
302
303
    /**
304
     * 即时写入日志
305
     */
306
    public function write()
307
    {
308
        if ($this->checkSize()) {
309
            $this->packLogFile();
310
        }
311
        $this->rollLatest();
312
        $this->removePackFiles();
313
    }
314
315
    /**
316
     * 程序关闭时调用
317
     */
318
    public function shutdown()
319
    {
320
        if (function_exists('fastcgi_finish_request')) {
321
            fastcgi_finish_request();
322
        }
323
        $this->write();
324
    }
325
326
    /**
327
     * @param string $message
328
     * @param array $context
329
     * @return string
330
     */
331
    public function interpolate(string $message, array $context)
332
    {
333
        $replace = [];
334
        foreach ($context as $key => $val) {
335
            if (is_bool($val)) {
336
                $val = $val ? 'true' : 'false';
337
            } elseif (null === $val) {
338
                $val = 'null';
339
            }
340
            $replace['{' . $key . '}'] = $val;
341
        }
342
        return strtr($message, $replace);
343
    }
344
}
345