Code Coverage |
||||||||||
Lines |
Functions and Methods |
Classes and Traits |
||||||||
Total | |
71.91% |
64 / 89 |
|
50.00% |
4 / 8 |
CRAP | |
0.00% |
0 / 1 |
Logging | |
71.91% |
64 / 89 |
|
50.00% |
4 / 8 |
59.62 | |
0.00% |
0 / 1 |
__construct | |
100.00% |
2 / 2 |
|
100.00% |
1 / 1 |
1 | |||
formatLevel | |
100.00% |
5 / 5 |
|
100.00% |
1 / 1 |
6 | |||
formatMessage | |
61.90% |
13 / 21 |
|
0.00% |
0 / 1 |
19.96 | |||
getRemoteHost | |
57.14% |
8 / 14 |
|
0.00% |
0 / 1 |
8.83 | |||
getLogParameters | |
100.00% |
14 / 14 |
|
100.00% |
1 / 1 |
1 | |||
format | |
100.00% |
15 / 15 |
|
100.00% |
1 / 1 |
1 | |||
toHeader | |
63.64% |
7 / 11 |
|
0.00% |
0 / 1 |
6.20 | |||
injectILogger | |
0.00% |
0 / 7 |
|
0.00% |
0 / 1 |
6 |
1 | <?php |
2 | |
3 | declare(strict_types=1); |
4 | |
5 | namespace PeServer\Core\Log; |
6 | |
7 | use DateTimeImmutable; |
8 | use DateTimeInterface; |
9 | use PeServer\Core\Collection\Arr; |
10 | use PeServer\Core\Cryptography; |
11 | use PeServer\Core\DI\DiItem; |
12 | use PeServer\Core\DI\IDiContainer; |
13 | use PeServer\Core\InitializeChecker; |
14 | use PeServer\Core\IO\Path; |
15 | use PeServer\Core\Log\ILogger; |
16 | use PeServer\Core\Log\ILoggerFactory; |
17 | use PeServer\Core\Store\SpecialStore; |
18 | use PeServer\Core\Text; |
19 | use PeServer\Core\Throws\NotImplementedException; |
20 | use PeServer\Core\TypeUtility; |
21 | |
22 | /** |
23 | * ロガー生成・共通処理。 |
24 | */ |
25 | class 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 | } |