Files
visit.waterschurch.org/public/api/lib/Log.php
T

87 lines
3.3 KiB
PHP
Raw Normal View History

<?php
declare(strict_types=1);
/**
* Minimal structured logger. Writes JSON lines to a private log file and
* redacts anything that looks like personal data or a secret.
*/
final class Log
{
private static string $file = '';
private static int $level = 1;
private const LEVELS = ['debug' => 0, 'info' => 1, 'warn' => 2, 'error' => 3];
private const REDACT_KEYS = ['email', 'phone', 'firstname', 'first_name', 'lastname', 'last_name', 'authorization', 'api_key', 'apikey', 'secret', 'token', 'password', 'questions', 'destination_number', 'email_address'];
public static function init(string $file, ?string $level = 'info'): void
{
self::$file = $file;
self::$level = self::LEVELS[strtolower($level ?? 'info')] ?? 1;
$dir = dirname($file);
if (!is_dir($dir)) {
@mkdir($dir, 0750, true);
}
}
/** @param array<string,mixed> $ctx */
public static function debug(string $msg, array $ctx = []): void { self::write('debug', $msg, $ctx); }
/** @param array<string,mixed> $ctx */
public static function info(string $msg, array $ctx = []): void { self::write('info', $msg, $ctx); }
/** @param array<string,mixed> $ctx */
public static function warn(string $msg, array $ctx = []): void { self::write('warn', $msg, $ctx); }
/** @param array<string,mixed> $ctx */
public static function error(string $msg, array $ctx = []): void { self::write('error', $msg, $ctx); }
/** @param array<string,mixed> $ctx */
private static function write(string $level, string $msg, array $ctx): void
{
if ((self::LEVELS[$level] ?? 1) < self::$level) {
return;
}
$line = json_encode([
'ts' => gmdate('c'),
'level' => $level,
'msg' => $msg,
'ctx' => self::redact($ctx),
], JSON_UNESCAPED_SLASHES) . "\n";
if (self::$file !== '' && (is_writable(dirname(self::$file)) || is_writable(self::$file))) {
@file_put_contents(self::$file, $line, FILE_APPEND | LOCK_EX);
} else {
error_log(rtrim($line));
}
}
/** @param mixed $value */
public static function redact($value, string $key = ''): mixed
{
if (is_array($value)) {
$out = [];
foreach ($value as $k => $v) {
$out[$k] = self::redact($v, (string)$k);
}
return $out;
}
if (is_string($value)) {
if (in_array(strtolower($key), self::REDACT_KEYS, true)) {
return self::mask($value);
}
// Inline redaction of emails, phone numbers and bearer-like tokens inside free text
$value = preg_replace('/[A-Z0-9._%+-]+@[A-Z0-9.-]+\.[A-Z]{2,}/i', '[email]', $value) ?? $value;
$value = preg_replace('/\+?\d[\d\s().-]{8,}\d/', '[phone]', $value) ?? $value;
return preg_replace('/(Basic|Bearer)\s+[A-Za-z0-9+\/=._-]{8,}/', '$1 [redacted]', $value) ?? $value;
}
return $value;
}
private static function mask(string $v): string
{
$len = strlen($v);
if ($len === 0) {
return '';
}
if ($len <= 4) {
return str_repeat('*', $len);
}
return substr($v, 0, 2) . str_repeat('*', max(1, $len - 4)) . substr($v, -2);
}
}