/[/...]. */ private const PATH_PATTERNS = [ '#(/api/v[0-9]+/time/account/)[^/\s"\']+#i', '#(/api/v[0-9]+/employees/)[^/\s"\']+#i', '#(/api/v[0-9]+/employee/departments/)[^/\s"\']+#i', ]; /** * @var string[] */ private readonly array $secrets; public function __construct(string $apiKey) { $secrets = []; // Short values would match far too much; a real key never is. if (8 <= strlen($apiKey)) { $secrets[$apiKey] = $apiKey; $secrets[rawurlencode($apiKey)] = rawurlencode($apiKey); } $this->secrets = array_values($secrets); } public function __invoke(LogRecord $record): LogRecord { $message = $this->redact($record->message); $context = $this->redactDeep($record->context); $extra = $this->redactDeep($record->extra); if ($message === $record->message && $context === $record->context && $extra === $record->extra) { return $record; } // LogRecord only allows some of its fields to be assigned through array access; // message is not one of them, so a redacted record is a new one. return $record->with(message: $message, context: $context, extra: $extra); } private function redactDeep(array $values, int $depth = 0): array { // Contexts hold arbitrary payloads; stop rather than walk something pathological. if (8 < $depth) { return $values; } foreach ($values as $key => $value) { if (is_string($value)) { $values[$key] = $this->redact($value); } elseif (is_array($value)) { $values[$key] = $this->redactDeep($value, $depth + 1); } } return $values; } private function redact(string $value): string { foreach ($this->secrets as $secret) { $value = str_replace($secret, self::PLACEHOLDER, $value); } foreach (self::PATH_PATTERNS as $pattern) { $value = (string) preg_replace($pattern, '$1'.self::PLACEHOLDER, $value); } return $value; } }