Passed
Push — master ( a93a57...e71b36 )
by 世昌
02:18
created

FileLogger::set()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 6
Code Lines 4

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 1
eloc 4
nc 1
nop 1
dl 0
loc 6
rs 10
c 0
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
        $msec = explode('.', microtime(true))[1];
93
        $save = $this->getConfig('save-path');
94
        $this->tempName = $save . '/' . date('YmdHis') . '.' . $msec . '.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
            if ($zip->addFile($logFile, date('Y-m-d') . '-' . $zip->numFiles . '.log')) {
134
                array_push($this->removeFiles, $logFile);
135
            }
136
            if (is_dir($this->getConfig('save-dump-path'))) {
137
                $it = new RecursiveIteratorIterator(new RecursiveDirectoryIterator(
138
                    $this->getConfig('save-dump-path'),
139
                    RecursiveDirectoryIterator::SKIP_DOTS
140
                ));
141
                foreach ($it as $dumpLog) {
142
                    if ($zip->addFile($dumpLog, 'dump/' . basename($dumpLog))) {
143
                        array_push($this->removeFiles, $dumpLog);
144
                    }
145
                }
146
            }
147
            $zip->close();
148
        } else {
149
            if (is_file($logFile) && file_exists($logFile)) {
150
                rename(
151
                    $logFile,
152
                    $this->getConfig('save-path')
153
                    . '/' . date('Y-m-d')
154
                    . '-' . substr(md5_file($logFile), 0, 8) . '.log'
155
                );
156
            }
157
        }
158
    }
159
160
    /**
161
     * 获取压缩
162
     *
163
     * @param string $path
164
     * @return ZipArchive|null
165
     */
166
    private function getZipArchive(string $path)
167
    {
168
        if (class_exists('ZipArchive')) {
169
            $zip = new ZipArchive;
170
            $res = $zip->open($path, ZipArchive::CREATE);
171
            if ($res === true) {
172
                return $zip;
173
            }
174
        }
175
        return null;
176
    }
177
178
    /**
179
     * 检查日志文件大小
180
     *
181
     * @return boolean
182
     */
183
    private function checkSize(): bool
184
    {
185
        $logFile = $this->latest;
186
        if (file_exists($logFile)) {
187
            if (filesize($logFile) > $this->getConfig('max-file-size')) {
188
                return true;
189
            }
190
        }
191
        return false;
192
    }
193
194
195
    /**
196
     * @param string $level
197
     * @param string $message
198
     * @param array $context
199
     * @return mixed|void
200
     * @throws FileLoggerException
201
     */
202
    public function log($level, string $message, array $context = [])
203
    {
204
        if (LogLevel::compare($level, $this->getConfig('log-level')) >= 0) {
205
            $replace = [];
206
            $message = $this->interpolate($message, $context);
207
            $replace['%level%'] = $level;
208
            $replace['%message%'] = $message;
209
            $write = strtr($this->getConfig('log-format'), $replace);
210
            fwrite($this->getAvailableWrite(), $write . PHP_EOL);
211
        }
212
    }
213
214
215
    /**
216
     * 将临时文件写入最后日志
217
     */
218
    private function rollLatest()
219
    {
220
        if (isset($this->latest)) {
221
            $size = ftell($this->temp);
222
            fseek($this->temp, 0);
223
            if ($size > 0) {
224
                $body = fread($this->temp, $size);
225
                file_put_contents($this->latest, $body, FILE_APPEND);
226
            }
227
            fclose($this->temp);
228
            $this->temp = null;
229
            if ($this->tempName !== null) {
230
                unlink($this->tempName);
231
                $this->tempName = null;
232
            }
233
        }
234
    }
235
236
    /**
237
     * 删除已经压缩的文件
238
     */
239
    private function removePackFiles()
240
    {
241
        foreach ($this->removeFiles as $file) {
242
            if (is_file($file) && file_exists($file)) {
243
                unlink($file);
244
            }
245
        }
246
        $this->removeFiles = [];
247
    }
248
249
    /**
250
     * 即时写入日志
251
     */
252
    public function write()
253
    {
254
        if ($this->checkSize()) {
255
            $this->packLogFile();
256
        }
257
        $this->rollLatest();
258
        $this->removePackFiles();
259
    }
260
261
    /**
262
     * 程序关闭时调用
263
     */
264
    public function shutdown()
265
    {
266
        if (function_exists('fastcgi_finish_request')) {
267
            fastcgi_finish_request();
268
        }
269
        $this->write();
270
    }
271
272
    /**
273
     * @param string $message
274
     * @param array $context
275
     * @return string
276
     */
277
    public function interpolate(string $message, array $context)
278
    {
279
        $replace = [];
280
        foreach ($context as $key => $val) {
281
            if (is_bool($val)) {
282
                $val = $val ? 'true' : 'false';
283
            } elseif (null === $val) {
284
                $val = 'null';
285
            }
286
            $replace['{' . $key . '}'] = $val;
287
        }
288
        return strtr($message, $replace);
289
    }
290
}
291