Code

< 40 %
40-60 %
> 60 %
1
<?php
2
3
namespace WebStream\Log;
4
5
use WebStream\IO\File;
6
use WebStream\IO\Reader\FileReader;
7
use WebStream\IO\Writer\SimpleFileWriter;
8
use WebStream\Container\Container;
9
use WebStream\Exception\Extend\IOException;
10
use WebStream\Exception\Extend\LoggerException;
11
use WebStream\Log\Outputter\ILazyWriter;
12
use WebStream\Log\Outputter\IOutputter;
13
14
/**
15
 * Loggerクラス
16
 * @author Ryuichi Tanaka
17
 * @since 2012/01/16
18
 * @version 0.7
19
 */
20
class Logger
21
{
22
    use LoggerUtils;
23
24
    /**
25
     * @var Logger ロガー
26
     */
27
    private static Logger $logger;
28
29
    /**
30
     * @var LoggerFormatter ロガーフォーマッタ
31
     */
32
    private static LoggerFormatter $formatter;
33
34
    /**
35
     * @var Container ログ設定コンテナ
36
     */
37
    private static Container $config;
38
39
    /**
40
     * @var Container ログ設定コンテナ
41
     */
42
    private Container $logConfig;
43
44
    /**
45
     * @var array<IOutputter> Outputterリスト
46
     */
47
    private array $outputters;
48
49
    /**
50
     * @var Container IOコンテナ
51
     */
52
    private Container $ioContainer;
53
54
    /**
55
     * @var File ログファイル
56
     */
57
    private File $logFile;
58
59
    /**
60
     * @var File ステータスファイル
61
     */
62
    private File $statusFile;
63
64
    /**
65 141
     * コンストラクタ
66
     * @param Container ログ設定コンテナ
67 141
     */
68 141
    private function __construct(Container $logConfig)
69
    {
70 141
        $this->logConfig = $logConfig;
71 141
        $this->outputters = [];
72
73 141
        $logFile = new File($logConfig->logPath);
74
        $statusFile = new File($logConfig->statusPath);
75 14
76 14
        $this->ioContainer = new Container();
77
78
        $this->ioContainer->statusReader = function () use ($statusFile) {
79
            return new FileReader($statusFile);
80
        };
81
        $this->ioContainer->statusWriter = function () use ($statusFile) {
82
            return new SimpleFileWriter($statusFile->getFilePath());
83
        };
84
        $this->ioContainer->logWriter = function () use ($logFile) {
85 141
            return new SimpleFileWriter($logFile->getFilePath());
86 141
        };
87 141
88
        $this->logFile = $logFile;
89
        $this->statusFile = $statusFile;
90
    }
91
92
    /**
93
     * デストラクタ
94
     */
95
    public function __destruct()
96
    {
97
        $this->directWrite();
98
    }
99
100
    /**
101
     * ログ設定を返却する
102
     * @return Container ログ設定
103
     */
104
    public function getConfig()
105
    {
106
        return $this->logConfig;
107
    }
108
109
    /**
110
     * 遅延書き出しを有効にする
111
     */
112
    public static function enableLazyWrite()
113
    {
114
        self::$logger->lazyWrite();
115
    }
116
117
    /**
118
     * 即時書き出しを有効にする
119
     */
120
    public static function enableDirectWrite()
121
    {
122
        self::$logger->directWrite();
123
    }
124
125
    /**
126 141
     * インスタンスを返却する
127
     * @return Logger ロガーインスタンス
128 141
     */
129
    public static function getInstance()
130
    {
131
        return self::$logger;
132
    }
133
134
    /**
135
     * ファイナライザ
136
     */
137
    public static function finalize()
138
    {
139
        self::$config = null;
140
        self::$logger = null;
141
        self::$formatter = null;
142
    }
143
144
    /**
145 141
     * Loggerを初期化する
146
     * @param Container $config
147 141
     */
148 141
    public static function init(Container $config)
149 141
    {
150 141
        self::$config = $config;
151
        self::$logger = new Logger($config);
152
        self::$formatter = new LoggerFormatter($config);
153
    }
154
155
    /**
156
     * Loggerが初期化済みかどうかチェックする
157
     * @return bool 初期化済みならtrue
158
     */
159
    public static function isInitialized()
160
    {
161
        return self::$logger !== null;
162
    }
163
164
    /**
165
     * Loggerメソッドの呼び出しを受ける
166
     * @param string メソッド名(ログレベル文字列)
167
     * @param array 引数
168
     */
169
    public static function __callStatic($level, $arguments)
170
    {
171
        if (self::$logger === null || self::$formatter === null) {
172
            if (self::$config !== null) {
173
                self::init(self::$config);
174
            } else {
175
                throw new LoggerException("Logger is not initialized.");
176
            }
177
        }
178
179
        call_user_func_array([self::$logger, "write"], array_merge([$level], $arguments));
180
    }
181
182
    /**
183 141
     * Outputterを設定する
184
     * @param array<IOutputter> $outputters Outputterリスト
185 141
     */
186 141
    public function setOutputter(array $outputters)
187
    {
188
        foreach ($outputters as $outputter) {
189
            if (!$outputter instanceof IOutputter) {
190 141
                throw new LoggerException("Log outputter must implement WebStream\Log\Outputter\IOutputter.");
191 141
            }
192
        }
193
        $this->outputters = $outputters;
194
    }
195
196
    /**
197
     * タイムスタンプを取得する
198
     * @return string タイムスタンプ
199
     */
200
    private function getTimeStamp()
201
    {
202
        date_default_timezone_set('Asia/Tokyo');
203
        $msec = sprintf("%2d", floatval(microtime()) * 100);
204
205
        return strftime("%Y-%m-%d %H:%M:%S") . "," . $msec;
206
    }
207
208
    /**
209
     * ログを書き出す
210
     * @param string ログレベル文字列
211 141
     * @param string 出力文字列
212
     * @param array<mixed> 埋め込み値リスト
213 141
     */
214 44
    public function write($level, $msg, $context = null)
215
    {
216
        if ($this->logConfig->logLevel > $this->toLogLevelValue($level)) {
217 97
            return;
218
        }
219
220 4
        if (is_array($context) && count($context) > 0) {
221 4
            // sprintfと同様の展開
222 4
            // [a-zA-Z0-9_-\.] 以外もキーには指定可能だが仕様としてこれ以外は不可とする
223 4
            preg_match_all('/\{\s*([a-zA-Z0-9._-]+)\s*?\}/', $msg, $matches);
224
            foreach ($matches[1] as $index => $value) {
225
                if (array_key_exists($value, $context)) {
226
                    $matches[1][$index] = $context[$value];
227
                } else {
228 4
                    unset($matches[0][$index]);
229
                }
230
            }
231
            $msg = str_replace($matches[0], $matches[1], $msg);
232 97
        }
233
234
        // ログローテート処理
235 97
        $this->rotate();
236 97
237 97
        try {
238
            if (count($this->outputters) > 0) {
239
                foreach ($this->outputters as $outputter) {
240
                    $outputter->write(self::$formatter->getFormattedMessage($msg, $level));
241
                }
242
            } else {
243
                $this->ioContainer->logWriter->write(self::$formatter->getFormattedMessage($msg, $level) . PHP_EOL);
244
            }
245 97
        } catch (IOException $e) {
246
            throw new LoggerException($e);
247
        }
248
    }
249
250
    /**
251
     * ログファイルをアーカイブする
252 97
     * stream.log -> stream.(作成日時)-(現在日時).log
253
     * @param string ログファイルパス
254
     */
255 97
    private function rotate()
256
    {
257
        // ログファイルがない場合はローテートしない
258
        if (!$this->logFile->exists()) {
259
            return;
260 97
        }
261 14
262 83
        // ログローテート実行
263
        if ($this->logConfig->rotateCycle !== null) {
264
            $this->rotateByCycle();
265 97
        } elseif ($this->logConfig->rotateSize !== null) {
266
            $this->rotateBySize();
267
        }
268
    }
269
270
    /**
271
     * ログステータスファイルに書きこむ
272
     * @throws IOException
273
     */
274
    private function writeStatus()
275
    {
276
        $this->ioContainer->statusWriter->write(intval(preg_replace('/^.*\s/', '', microtime())));
277
    }
278
279
    /**
280
     * ログステータスファイルを読み込む
281 14
     * @return int UnixTime
282
     * @throws LoggerException
283 14
     */
284 14
    private function readStatus()
285
    {
286
        $content = $this->ioContainer->statusReader->read();
287
        if (!preg_match('/^\d{10}$/', $content)) {
288 14
            throw new LoggerException("Invalid log state file contents: " . $content);
289
        }
290
291
        return intval($content);
292
    }
293
294 14
    /**
295
     * ステータスファイルを作成する
296
     */
297 14
    private function createStatusFile()
298
    {
299
        // ステータスファイルがない場合は書きだす
300 14
        if (!$this->statusFile->exists()) {
301
            $this->writeStatus();
302
        }
303
    }
304
305
    /**
306
     * ローテートを実行する
307 8
     * @param int 作成日時のUnixTime
308
     * @param int 現在日時のUnixTime
309 8
     */
310 8
    private function runRotate($from, $to)
311 8
    {
312 8
        $fromDate = date("YmdHis", $from);
313 8
        $toDate = date("YmdHis", $to);
314
        $archivePath = null;
315
        if (preg_match('/(.*)\.(.+)/', $this->logConfig->logPath, $matches)) {
316
            $archivePath = "$matches[1].${fromDate}-${toDate}.$matches[2]";
317
            // mvを実行
318
            $this->logFile->renameTo($archivePath);
319 8
            // ステータスファイルを削除
320
            $this->statusFile->delete();
321
        }
322
    }
323
324
    /**
325
     * 時間単位でローテートする
326
     * stream.log -> stream.(作成日時)-(現在日時).log
327 14
     */
328 14
    private function rotateByCycle()
329 14
    {
330
        $this->createStatusFile();
331 14
        $now = intval(preg_replace('/^.*\s/', '', microtime()));
332 14
        $createdAt = $this->readStatus();
333 8
334
        $hour = intval(floor(($now - $createdAt) / 3600));
335 14
        if ($hour >= $this->logConfig->rotateCycle) {
336
            $this->runRotate($createdAt, $now);
337
        }
338
    }
339
340
    /**
341
     * サイズ単位でローテートする
342
     * stream.log -> stream.(作成日時)-(現在日時).log
343
     */
344
    private function rotateBySize()
345
    {
346
        $this->createStatusFile();
347
        $now = intval(preg_replace('/^.*\s/', '', microtime()));
348
        $createdAt = $this->readStatus();
349
350
        $sizeKb = (int) floor($this->logFile->length() / 1024);
351
        // 指定したサイズより大きければローテート
352
        if ($sizeKb >= $this->logConfig->rotateSize) {
353
            $this->runRotate($createdAt, $now);
354
        }
355
    }
356
357
    /**
358
     * ログ出力パスを返却する
359
     * @return string ログ出力パス
360
     */
361
    public function getLogPath()
362
    {
363
        return $this->logConfig->logPath;
364
    }
365
366
    /**
367
     * ログローテートサイクルを返却する
368
     * @return string ログ出力パス
369
     */
370
    public function getLogRotateCycle()
371
    {
372
        return $this->logConfig->rotateCycle;
373
    }
374
375
    /**
376
     * ログローテートサイズを返却する
377
     * @return string ログ出力パス
378
     */
379
    public function getLogRotateSize()
380
    {
381
        return $this->logConfig->rotateSize;
382
    }
383
384
    /**
385
     * 遅延書き出しを有効にする
386
     */
387
    public function lazyWrite()
388
    {
389
        foreach ($this->outputters as $outputter) {
390
            if ($outputter instanceof ILazyWriter) {
391
                $outputter->enableLazyWrite();
392
            }
393
        }
394
    }
395
396
    /**
397
     * 即時書き出しを有効にする
398 1
     */
399 1
    public function directWrite()
400 1
    {
401
        foreach ($this->outputters as $outputter) {
402
            if ($outputter instanceof ILazyWriter) {
403 1
                $outputter->enableDirectWrite();
404
            }
405
        }
406
    }
407
}
408