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 $ctx */ public static function debug(string $msg, array $ctx = []): void { self::write('debug', $msg, $ctx); } /** @param array $ctx */ public static function info(string $msg, array $ctx = []): void { self::write('info', $msg, $ctx); } /** @param array $ctx */ public static function warn(string $msg, array $ctx = []): void { self::write('warn', $msg, $ctx); } /** @param array $ctx */ public static function error(string $msg, array $ctx = []): void { self::write('error', $msg, $ctx); } /** @param array $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); } }