Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
72.53% covered (warning)
72.53%
66 / 91
50.00% covered (danger)
50.00%
4 / 8
CRAP
0.00% covered (danger)
0.00%
0 / 1
Logging
72.53% covered (warning)
72.53%
66 / 91
50.00% covered (danger)
50.00%
4 / 8
57.97
0.00% covered (danger)
0.00%
0 / 1
 __construct
100.00% covered (success)
100.00%
2 / 2
100.00% covered (success)
100.00%
1 / 1
1
 formatLevel
100.00% covered (success)
100.00%
7 / 7
100.00% covered (success)
100.00%
1 / 1
6
 formatMessage
61.90% covered (warning)
61.90%
13 / 21
0.00% covered (danger)
0.00%
0 / 1
19.96
 getRemoteHost
57.14% covered (warning)
57.14%
8 / 14
0.00% covered (danger)
0.00%
0 / 1
8.83
 getLogParameters
100.00% covered (success)
100.00%
14 / 14
100.00% covered (success)
100.00%
1 / 1
1
 format
100.00% covered (success)
100.00%
15 / 15
100.00% covered (success)
100.00%
1 / 1
1
 toHeader
63.64% covered (warning)
63.64%
7 / 11
0.00% covered (danger)
0.00%
0 / 1
6.20
 injectILogger
0.00% covered (danger)
0.00%
0 / 7
0.00% covered (danger)
0.00%
0 / 1
6
1<?php
2
3declare(strict_types=1);
4
5namespace PeServer\Core\Log;
6
7use DateTimeImmutable;
8use DateTimeInterface;
9use PeServer\Core\Collection\Arr;
10use PeServer\Core\Cryptography;
11use PeServer\Core\DI\DiItem;
12use PeServer\Core\DI\IDiContainer;
13use PeServer\Core\InitializeChecker;
14use PeServer\Core\IO\Path;
15use PeServer\Core\Log\ILogger;
16use PeServer\Core\Log\ILoggerFactory;
17use PeServer\Core\Store\SpecialStore;
18use PeServer\Core\Text;
19use PeServer\Core\Throws\NotImplementedException;
20use PeServer\Core\TypeUtility;
21
22/**
23 * ロガー生成・共通処理。
24 */
25class Logging
26{
27    #region define
28
29    private const LOG_REQUEST_ID_LENGTH = 6;
30    private const IS_ENABLED_HOST = true;
31
32    #endregion
33
34    #region variable
35
36    private string $requestId;
37    private ?string $requestHost = null;
38
39    private SpecialStore $specialStore;
40
41    #endregion
42
43    #region function
44
45    public function __construct(SpecialStore $specialStore)
46    {
47        $this->requestId = Cryptography::generateRandomBinary(self::LOG_REQUEST_ID_LENGTH)->toHex();
48        $this->specialStore = $specialStore;
49    }
50
51    /**
52     * ログレベル書式化。
53     *
54     * @param int $level
55     * @phpstan-param ILogger::LOG_LEVEL_* $level
56     * @return string
57     */
58    private static function formatLevel(int $level): string
59    {
60        return match ($level) {
61            ILogger::LOG_LEVEL_TRACE => 'TRACE',
62            ILogger::LOG_LEVEL_DEBUG => 'DEBUG',
63            ILogger::LOG_LEVEL_INFORMATION => 'INFO ',
64            ILogger::LOG_LEVEL_WARNING => 'WARN ',
65            ILogger::LOG_LEVEL_ERROR => 'ERROR',
66        };
67    }
68
69    /**
70     * メッセージ書式適用。
71     *
72     * @param mixed $message
73     * @phpstan-param LogMessageAlias $message
74     * @param mixed ...$parameters
75     * @return string
76     */
77    private static function formatMessage($message, ...$parameters): string
78    {
79        if ($message === null) {
80            if (Arr::isNullOrEmpty($parameters)) {
81                return Text::EMPTY;
82            }
83            return Text::dump($parameters);
84        }
85
86        if (is_string($message) && !Arr::isNullOrEmpty($parameters) && array_keys($parameters)[0] === 0) {
87            /** @var string[] */
88            $values = array_map(function ($value) {
89                if (is_string($value)) {
90                    return $value;
91                }
92                if (is_object($value) || is_array($value)) {
93                    return Text::dump($value);
94                }
95
96                return strval($value);
97            }, $parameters);
98
99            /** @var array<string,string> */
100            $map = [];
101            foreach ($values as $key => $value) {
102                $map[strval($key)] = $value;
103            }
104
105            return Text::replaceMap($message, $map);
106        }
107
108        if (Arr::isNullOrEmpty($parameters)) {
109            if (is_string($message)) {
110                return $message;
111            }
112
113            return Text::dump($message);
114        }
115        return Text::dump(['message' => $message, 'parameters' => $parameters]);
116    }
117
118    private function getRemoteHost(): string
119    {
120        // @phpstan-ignore-next-line
121        if (!self::IS_ENABLED_HOST) {
122            return Text::EMPTY;
123        }
124
125        if ($this->requestHost !== null) {
126            return $this->requestHost;
127        }
128
129        /** @var string */
130        $serverRemoteHost = $this->specialStore->getServer('REMOTE_HOST', Text::EMPTY);
131        if ($serverRemoteHost !== Text::EMPTY) {
132            return $this->requestHost = $serverRemoteHost;
133        }
134
135        /** @var string */
136        $serverRemoteIpAddr = $this->specialStore->getServer('REMOTE_ADDR', Text::EMPTY);
137        if ($serverRemoteIpAddr === Text::EMPTY) {
138            return $this->requestHost = Text::EMPTY;
139        }
140
141        $hostName = gethostbyaddr($serverRemoteIpAddr);
142        if ($hostName === false) {
143            return $this->requestHost = Text::EMPTY;
144        }
145
146        return $this->requestHost = $hostName;
147    }
148
149    /**
150     * ログで使う共通的なやつら
151     *
152     * @param DateTimeInterface $timestamp
153     * @param SpecialStore $specialStore
154     * @return array{TIMESTAMP:string,DATE:string,TIME:string,TIMEZONE:string,CLIENT_IP:string,CLIENT_HOST:string,REQUEST_ID:string,UA:string,METHOD:string,REQUEST:string,SESSION:string|false}
155     */
156    public function getLogParameters(DateTimeInterface $timestamp, SpecialStore $specialStore): array
157    {
158        return [
159            'TIMESTAMP' => $timestamp->format('c'),
160            'DATE' => $timestamp->format('Y-m-d'),
161            'TIME' => $timestamp->format('H:i:s'),
162            'TIMEZONE' => $timestamp->format('P'),
163            'CLIENT_IP' => $specialStore->getServer('REMOTE_ADDR', Text::EMPTY),
164            'CLIENT_HOST' => self::getRemoteHost(),
165            'REQUEST_ID' => $this->requestId,
166            'UA' => $specialStore->getServer('HTTP_USER_AGENT', Text::EMPTY),
167            'METHOD' => $specialStore->getServer('REQUEST_METHOD', Text::EMPTY),
168            'REQUEST' => $specialStore->getServer('REQUEST_URI', Text::EMPTY),
169            'SESSION' => session_id(),
170            'REFERER' => $specialStore->getServer('HTTP_REFERER', Text::EMPTY),
171        ];
172    }
173
174    /**
175     * ログ書式適用。
176     *
177     * @param string $format
178     * @phpstan-param literal-string $format
179     * @param int $level
180     * @phpstan-param ILogger::LOG_LEVEL_* $level 有効レベル。S
181     * @param int $level
182     * @param int $traceIndex
183     * @phpstan-param non-negative-int $traceIndex
184     * @param non-empty-string $header
185     * @param mixed $message
186     * @phpstan-param LogMessageAlias $message
187     * @param mixed ...$parameters
188     * @return string
189     */
190    public function format(string $format, int $level, int $traceIndex, DateTimeInterface $timestamp, string $header, $message, ...$parameters): string
191    {
192        /** @var array<string,array<string,mixed>>[] */
193        $backtrace = debug_backtrace(DEBUG_BACKTRACE_IGNORE_ARGS); // DEBUG_BACKTRACE_PROVIDE_OBJECT
194        /** @var array<string,mixed> */
195        $traceCaller = $backtrace[$traceIndex];
196        /** @var array<string,mixed> */
197        $traceMethod = $backtrace[$traceIndex + 1];
198
199        /** @var string */
200        $filePath = $traceCaller['file'] ?? Text::EMPTY;
201
202        /** @var array<string,string> */
203        $map = [
204            ...self::getLogParameters($timestamp, $this->specialStore),
205            //-------------------
206            'FILE' => $filePath,
207            'FILE_NAME' => Path::getFileName($filePath),
208            'LINE' => $traceCaller['line'] ?? 0,
209            //'CLASS' => $traceMethod['class'] ?? Text::EMPTY,
210            'FUNCTION' => $traceMethod['function'] ?? Text::EMPTY,
211            //'ARGS' => $traceMethod['args'] ?? Text::EMPTY,
212            //-------------------
213            'LEVEL' => self::formatLevel($level),
214            'HEADER' => $header,
215            'MESSAGE' => self::formatMessage($message, ...$parameters),
216        ];
217
218        return Text::replaceMap($format, $map);
219    }
220
221    /**
222     * ヘッダ名生成。
223     *
224     * @param string|object $input
225     * @return non-empty-string
226     */
227    public static function toHeader(string|object $input): string
228    {
229        $header = is_string($input)
230            ? $input
231            : TypeUtility::getType($input);
232
233        if (Text::contains($header, '\\', false)) {
234            $names = Text::split($header, '\\');
235            $name = $names[count($names) - 1];
236            if (!Text::isNullOrEmpty($name)) {
237                return $name;
238            }
239        }
240
241        if (Text::isNullOrEmpty($header)) {
242            return TypeUtility::getType($input);
243        }
244
245        return $header;
246    }
247
248    /**
249     * ILoggerに対して注入処理。
250     *
251     * @param IDiContainer $container
252     * @param DiItem[] $callStack
253     * @return ILogger
254     */
255    public static function injectILogger(IDiContainer $container, array $callStack): ILogger
256    {
257        $loggerFactory = $container->get(ILoggerFactory::class);
258        if (/*1 < */count($callStack)) {
259            //$item = $callStack[count($callStack) - 2];
260            $item = $callStack[0];
261            $className = (string)$item->data; // あぶねぇかなぁ
262            $header = Logging::toHeader($className);
263            return $loggerFactory->createLogger($header, 0);
264        }
265        return $loggerFactory->createLogger('<UNKNOWN>');
266    }
267
268    #endregion
269}