Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
100.00% covered (success)
100.00%
33 / 33
100.00% covered (success)
100.00%
5 / 5
CRAP
100.00% covered (success)
100.00%
1 / 1
CentralLogger
100.00% covered (success)
100.00%
33 / 33
100.00% covered (success)
100.00%
5 / 5
11
100.00% covered (success)
100.00%
1 / 1
 logAppError
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 logSecurity
100.00% covered (success)
100.00%
9 / 9
100.00% covered (success)
100.00%
1 / 1
2
 logSql
100.00% covered (success)
100.00%
8 / 8
100.00% covered (success)
100.00%
1 / 1
1
 logJsError
100.00% covered (success)
100.00%
7 / 7
100.00% covered (success)
100.00%
1 / 1
2
 write
100.00% covered (success)
100.00%
8 / 8
100.00% covered (success)
100.00%
1 / 1
5
1<?php
2
3declare(strict_types=1);
4
5namespace App\Shared\Logging;
6
7 class CentralLogger
8{
9    private const DEFAULT_IP = '127.0.0.1';
10    private const DEFAULT_USER_AGENT = 'Unknown';
11    private const DEFAULT_URI = '/';
12
13    private static string $logDir = '/var/log/ammonly';
14    private static string $fallbackDir = __DIR__ . '/../../../runtime/logs';
15
16    /**
17     * @param array<string, mixed> $context
18     */
19    public static function logAppError(string $message, array $context = []): void
20    {
21        self::write('app_error.log', 'ERROR', $message, $context);
22    }
23
24    /**
25     * @param array<string, mixed> $details
26     */
27    public static function logSecurity(string $event, array $details = []): void
28    {
29        $context = array_merge([
30            'ip' => $_SERVER['REMOTE_ADDR'] ?? self::DEFAULT_IP,
31            'user_agent' => $_SERVER['HTTP_USER_AGENT'] ?? self::DEFAULT_USER_AGENT,
32            'request_uri' => $_SERVER['REQUEST_URI'] ?? self::DEFAULT_URI,
33            'method' => $_SERVER['REQUEST_METHOD'] ?? 'GET',
34            'referer' => $_SERVER['HTTP_REFERER'] ?? 'DIRECT',
35            'session_id' => session_id() ?: 'NONE',
36        ], $details);
37
38        self::write('security.log', 'SECURITY_AUDIT', $event, $context);
39    }
40
41    /**
42     * @param array<string, mixed> $params
43     */
44    public static function logSql(string $query, array $params = [], float $executionTimeMs = 0.0): void
45    {
46        $context = [
47            'query' => $query,
48            'params' => $params,
49            'execution_time_ms' => $executionTimeMs,
50            'ip' => $_SERVER['REMOTE_ADDR'] ?? self::DEFAULT_IP,
51            'request_uri' => $_SERVER['REQUEST_URI'] ?? self::DEFAULT_URI,
52        ];
53
54        self::write('sql.log', 'SQL', 'Database Query', $context);
55    }
56
57    /**
58     * @param array<string, mixed> $payload
59     */
60    public static function logJsError(array $payload): void
61    {
62        $context = array_merge([
63            'ip' => $_SERVER['REMOTE_ADDR'] ?? self::DEFAULT_IP,
64            'user_agent' => $_SERVER['HTTP_USER_AGENT'] ?? self::DEFAULT_USER_AGENT,
65            'referer' => $_SERVER['HTTP_REFERER'] ?? 'DIRECT',
66        ], $payload);
67
68        $msg = is_string($payload['message'] ?? null) ? (string) $payload['message'] : 'Client JS Exception';
69        self::write('js_error.log', 'JS_ERROR', $msg, $context);
70    }
71
72    /**
73     * @param array<string, mixed> $context
74     */
75    private static function write(string $filename, string $level, string $message, array $context = []): void
76    {
77        $dir = is_dir(self::$logDir) && is_writable(self::$logDir) ? self::$logDir : self::$fallbackDir;
78        if (!is_dir($dir)) {
79            @mkdir($dir, 0o777, true);
80        }
81
82        $logFile = $dir . '/' . $filename;
83        $timestamp = date('Y-m-d H:i:s');
84        $jsonContext = !empty($context) ? json_encode($context, JSON_UNESCAPED_SLASHES | JSON_UNESCAPED_UNICODE) : '{}';
85        $logLine = sprintf("[%s] [%s] %s %s\n", $timestamp, $level, $message, $jsonContext);
86
87        @file_put_contents($logFile, $logLine, FILE_APPEND | LOCK_EX);
88    }
89}